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