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