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