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