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