Home | History | Annotate | Download | only in hdmi
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.hdmi;
     18 
     19 import android.hardware.hdmi.HdmiControlManager;
     20 import android.hardware.hdmi.HdmiDeviceInfo;
     21 import android.hardware.hdmi.IHdmiControlCallback;
     22 import android.os.PowerManager;
     23 import android.os.PowerManager.WakeLock;
     24 import android.os.RemoteException;
     25 import android.os.SystemProperties;
     26 import android.provider.Settings.Global;
     27 import android.util.Slog;
     28 
     29 import com.android.internal.app.LocalePicker;
     30 import com.android.internal.app.LocalePicker.LocaleInfo;
     31 import com.android.internal.util.IndentingPrintWriter;
     32 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
     33 
     34 import java.io.UnsupportedEncodingException;
     35 import java.util.List;
     36 import java.util.Locale;
     37 
     38 import java.util.List;
     39 
     40 /**
     41  * Represent a logical device of type Playback residing in Android system.
     42  */
     43 final class HdmiCecLocalDevicePlayback extends HdmiCecLocalDevice {
     44     private static final String TAG = "HdmiCecLocalDevicePlayback";
     45 
     46     private static final boolean WAKE_ON_HOTPLUG =
     47             SystemProperties.getBoolean(Constants.PROPERTY_WAKE_ON_HOTPLUG, true);
     48 
     49     private static final boolean SET_MENU_LANGUAGE =
     50             SystemProperties.getBoolean(Constants.PROPERTY_SET_MENU_LANGUAGE, false);
     51 
     52     private boolean mIsActiveSource = false;
     53 
     54     // Used to keep the device awake while it is the active source. For devices that
     55     // cannot wake up via CEC commands, this address the inconvenience of having to
     56     // turn them on. True by default, and can be disabled (i.e. device can go to sleep
     57     // in active device status) by explicitly setting the system property
     58     // persist.sys.hdmi.keep_awake to false.
     59     // Lazily initialized - should call getWakeLock() to get the instance.
     60     private ActiveWakeLock mWakeLock;
     61 
     62     // If true, turn off TV upon standby. False by default.
     63     private boolean mAutoTvOff;
     64 
     65     HdmiCecLocalDevicePlayback(HdmiControlService service) {
     66         super(service, HdmiDeviceInfo.DEVICE_PLAYBACK);
     67 
     68         mAutoTvOff = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, false);
     69 
     70         // The option is false by default. Update settings db as well to have the right
     71         // initial setting on UI.
     72         mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, mAutoTvOff);
     73     }
     74 
     75     @Override
     76     @ServiceThreadOnly
     77     protected void onAddressAllocated(int logicalAddress, int reason) {
     78         assertRunOnServiceThread();
     79         mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
     80                 mAddress, mService.getPhysicalAddress(), mDeviceType));
     81         mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
     82                 mAddress, mService.getVendorId()));
     83         startQueuedActions();
     84     }
     85 
     86     @Override
     87     @ServiceThreadOnly
     88     protected int getPreferredAddress() {
     89         assertRunOnServiceThread();
     90         return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
     91                 Constants.ADDR_UNREGISTERED);
     92     }
     93 
     94     @Override
     95     @ServiceThreadOnly
     96     protected void setPreferredAddress(int addr) {
     97         assertRunOnServiceThread();
     98         SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
     99                 String.valueOf(addr));
    100     }
    101 
    102     @ServiceThreadOnly
    103     void oneTouchPlay(IHdmiControlCallback callback) {
    104         assertRunOnServiceThread();
    105         List<OneTouchPlayAction> actions = getActions(OneTouchPlayAction.class);
    106         if (!actions.isEmpty()) {
    107             Slog.i(TAG, "oneTouchPlay already in progress");
    108             actions.get(0).addCallback(callback);
    109             return;
    110         }
    111         OneTouchPlayAction action = OneTouchPlayAction.create(this, Constants.ADDR_TV,
    112                 callback);
    113         if (action == null) {
    114             Slog.w(TAG, "Cannot initiate oneTouchPlay");
    115             invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION);
    116             return;
    117         }
    118         addAndStartAction(action);
    119     }
    120 
    121     @ServiceThreadOnly
    122     void queryDisplayStatus(IHdmiControlCallback callback) {
    123         assertRunOnServiceThread();
    124         List<DevicePowerStatusAction> actions = getActions(DevicePowerStatusAction.class);
    125         if (!actions.isEmpty()) {
    126             Slog.i(TAG, "queryDisplayStatus already in progress");
    127             actions.get(0).addCallback(callback);
    128             return;
    129         }
    130         DevicePowerStatusAction action = DevicePowerStatusAction.create(this, Constants.ADDR_TV,
    131                 callback);
    132         if (action == null) {
    133             Slog.w(TAG, "Cannot initiate queryDisplayStatus");
    134             invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION);
    135             return;
    136         }
    137         addAndStartAction(action);
    138     }
    139 
    140     @ServiceThreadOnly
    141     private void invokeCallback(IHdmiControlCallback callback, int result) {
    142         assertRunOnServiceThread();
    143         try {
    144             callback.onComplete(result);
    145         } catch (RemoteException e) {
    146             Slog.e(TAG, "Invoking callback failed:" + e);
    147         }
    148     }
    149 
    150     @Override
    151     @ServiceThreadOnly
    152     void onHotplug(int portId, boolean connected) {
    153         assertRunOnServiceThread();
    154         mCecMessageCache.flushAll();
    155         // We'll not clear mIsActiveSource on the hotplug event to pass CETC 11.2.2-2 ~ 3.
    156         if (WAKE_ON_HOTPLUG && connected && mService.isPowerStandbyOrTransient()) {
    157             mService.wakeUp();
    158         }
    159         if (!connected) {
    160             getWakeLock().release();
    161         }
    162     }
    163 
    164     @Override
    165     @ServiceThreadOnly
    166     protected void onStandby(boolean initiatedByCec, int standbyAction) {
    167         assertRunOnServiceThread();
    168         if (!mService.isControlEnabled() || initiatedByCec || !mAutoTvOff) {
    169             return;
    170         }
    171         switch (standbyAction) {
    172             case HdmiControlService.STANDBY_SCREEN_OFF:
    173                 mService.sendCecCommand(
    174                         HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_TV));
    175                 break;
    176             case HdmiControlService.STANDBY_SHUTDOWN:
    177                 // ACTION_SHUTDOWN is taken as a signal to power off all the devices.
    178                 mService.sendCecCommand(
    179                         HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_BROADCAST));
    180                 break;
    181         }
    182     }
    183 
    184     @Override
    185     @ServiceThreadOnly
    186     void setAutoDeviceOff(boolean enabled) {
    187         assertRunOnServiceThread();
    188         mAutoTvOff = enabled;
    189     }
    190 
    191     @ServiceThreadOnly
    192     void setActiveSource(boolean on) {
    193         assertRunOnServiceThread();
    194         mIsActiveSource = on;
    195         if (on) {
    196             getWakeLock().acquire();
    197         } else {
    198             getWakeLock().release();
    199         }
    200     }
    201 
    202     @ServiceThreadOnly
    203     private ActiveWakeLock getWakeLock() {
    204         assertRunOnServiceThread();
    205         if (mWakeLock == null) {
    206             if (SystemProperties.getBoolean(Constants.PROPERTY_KEEP_AWAKE, true)) {
    207                 mWakeLock = new SystemWakeLock();
    208             } else {
    209                 // Create a dummy lock object that doesn't do anything about wake lock,
    210                 // hence allows the device to go to sleep even if it's the active source.
    211                 mWakeLock = new ActiveWakeLock() {
    212                     @Override
    213                     public void acquire() { }
    214                     @Override
    215                     public void release() { }
    216                     @Override
    217                     public boolean isHeld() { return false; }
    218                 };
    219                 HdmiLogger.debug("No wakelock is used to keep the display on.");
    220             }
    221         }
    222         return mWakeLock;
    223     }
    224 
    225     @Override
    226     protected boolean canGoToStandby() {
    227         return !getWakeLock().isHeld();
    228     }
    229 
    230     @Override
    231     @ServiceThreadOnly
    232     protected boolean handleActiveSource(HdmiCecMessage message) {
    233         assertRunOnServiceThread();
    234         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
    235         mayResetActiveSource(physicalAddress);
    236         return true;  // Broadcast message.
    237     }
    238 
    239     private void mayResetActiveSource(int physicalAddress) {
    240         if (physicalAddress != mService.getPhysicalAddress()) {
    241             setActiveSource(false);
    242         }
    243     }
    244 
    245     @ServiceThreadOnly
    246     protected boolean handleUserControlPressed(HdmiCecMessage message) {
    247         assertRunOnServiceThread();
    248         wakeUpIfActiveSource();
    249         return super.handleUserControlPressed(message);
    250     }
    251 
    252     @Override
    253     @ServiceThreadOnly
    254     protected boolean handleSetStreamPath(HdmiCecMessage message) {
    255         assertRunOnServiceThread();
    256         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
    257         maySetActiveSource(physicalAddress);
    258         maySendActiveSource(message.getSource());
    259         wakeUpIfActiveSource();
    260         return true;  // Broadcast message.
    261     }
    262 
    263     // Samsung model we tested sends <Routing Change> and <Request Active Source>
    264     // in a row, and then changes the input to the internal source if there is no
    265     // <Active Source> in response. To handle this, we'll set ActiveSource aggressively.
    266     @Override
    267     @ServiceThreadOnly
    268     protected boolean handleRoutingChange(HdmiCecMessage message) {
    269         assertRunOnServiceThread();
    270         int newPath = HdmiUtils.twoBytesToInt(message.getParams(), 2);
    271         maySetActiveSource(newPath);
    272         return true;  // Broadcast message.
    273     }
    274 
    275     @Override
    276     @ServiceThreadOnly
    277     protected boolean handleRoutingInformation(HdmiCecMessage message) {
    278         assertRunOnServiceThread();
    279         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
    280         maySetActiveSource(physicalAddress);
    281         return true;  // Broadcast message.
    282     }
    283 
    284     private void maySetActiveSource(int physicalAddress) {
    285         setActiveSource(physicalAddress == mService.getPhysicalAddress());
    286     }
    287 
    288     private void wakeUpIfActiveSource() {
    289         if (!mIsActiveSource) {
    290             return;
    291         }
    292         // Wake up the device if the power is in standby mode, or its screen is off -
    293         // which can happen if the device is holding a partial lock.
    294         if (mService.isPowerStandbyOrTransient() || !mService.getPowerManager().isScreenOn()) {
    295             mService.wakeUp();
    296         }
    297     }
    298 
    299     private void maySendActiveSource(int dest) {
    300         if (mIsActiveSource) {
    301             mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(
    302                     mAddress, mService.getPhysicalAddress()));
    303             // Always reports menu-status active to receive RCP.
    304             mService.sendCecCommand(HdmiCecMessageBuilder.buildReportMenuStatus(
    305                     mAddress, dest, Constants.MENU_STATE_ACTIVATED));
    306         }
    307     }
    308 
    309     @Override
    310     @ServiceThreadOnly
    311     protected boolean handleRequestActiveSource(HdmiCecMessage message) {
    312         assertRunOnServiceThread();
    313         maySendActiveSource(message.getSource());
    314         return true;  // Broadcast message.
    315     }
    316 
    317     @ServiceThreadOnly
    318     protected boolean handleSetMenuLanguage(HdmiCecMessage message) {
    319         assertRunOnServiceThread();
    320         if (!SET_MENU_LANGUAGE) {
    321             return false;
    322         }
    323 
    324         try {
    325             String iso3Language = new String(message.getParams(), 0, 3, "US-ASCII");
    326             Locale currentLocale = mService.getContext().getResources().getConfiguration().locale;
    327             if (currentLocale.getISO3Language().equals(iso3Language)) {
    328                 // Do not switch language if the new language is the same as the current one.
    329                 // This helps avoid accidental country variant switching from en_US to en_AU
    330                 // due to the limitation of CEC. See the warning below.
    331                 return true;
    332             }
    333 
    334             // Don't use Locale.getAvailableLocales() since it returns a locale
    335             // which is not available on Settings.
    336             final List<LocaleInfo> localeInfos = LocalePicker.getAllAssetLocales(
    337                     mService.getContext(), false);
    338             for (LocaleInfo localeInfo : localeInfos) {
    339                 if (localeInfo.getLocale().getISO3Language().equals(iso3Language)) {
    340                     // WARNING: CEC adopts ISO/FDIS-2 for language code, while Android requires
    341                     // additional country variant to pinpoint the locale. This keeps the right
    342                     // locale from being chosen. 'eng' in the CEC command, for instance,
    343                     // will always be mapped to en-AU among other variants like en-US, en-GB,
    344                     // an en-IN, which may not be the expected one.
    345                     LocalePicker.updateLocale(localeInfo.getLocale());
    346                     return true;
    347                 }
    348             }
    349             Slog.w(TAG, "Can't handle <Set Menu Language> of " + iso3Language);
    350             return false;
    351         } catch (UnsupportedEncodingException e) {
    352             Slog.w(TAG, "Can't handle <Set Menu Language>", e);
    353             return false;
    354         }
    355     }
    356 
    357     @Override
    358     protected int findKeyReceiverAddress() {
    359         return Constants.ADDR_TV;
    360     }
    361 
    362     @Override
    363     @ServiceThreadOnly
    364     protected void sendStandby(int deviceId) {
    365         assertRunOnServiceThread();
    366 
    367         // Playback device can send <Standby> to TV only. Ignore the parameter.
    368         int targetAddress = Constants.ADDR_TV;
    369         mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
    370     }
    371 
    372     @Override
    373     @ServiceThreadOnly
    374     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
    375         super.disableDevice(initiatedByCec, callback);
    376 
    377         assertRunOnServiceThread();
    378         if (!initiatedByCec && mIsActiveSource) {
    379             mService.sendCecCommand(HdmiCecMessageBuilder.buildInactiveSource(
    380                     mAddress, mService.getPhysicalAddress()));
    381         }
    382         setActiveSource(false);
    383         checkIfPendingActionsCleared();
    384     }
    385 
    386     @Override
    387     protected void dump(final IndentingPrintWriter pw) {
    388         super.dump(pw);
    389         pw.println("mIsActiveSource: " + mIsActiveSource);
    390         pw.println("mAutoTvOff:" + mAutoTvOff);
    391     }
    392 
    393     // Wrapper interface over PowerManager.WakeLock
    394     private interface ActiveWakeLock {
    395         void acquire();
    396         void release();
    397         boolean isHeld();
    398     }
    399 
    400     private class SystemWakeLock implements ActiveWakeLock {
    401         private final WakeLock mWakeLock;
    402         public SystemWakeLock() {
    403             mWakeLock = mService.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    404             mWakeLock.setReferenceCounted(false);
    405         }
    406 
    407         @Override
    408         public void acquire() {
    409             mWakeLock.acquire();
    410             HdmiLogger.debug("active source: %b. Wake lock acquired", mIsActiveSource);
    411         }
    412 
    413         @Override
    414         public void release() {
    415             mWakeLock.release();
    416             HdmiLogger.debug("Wake lock released");
    417         }
    418 
    419         @Override
    420         public boolean isHeld() {
    421             return mWakeLock.isHeld();
    422         }
    423     }
    424 }
    425