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