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