/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.hdmi; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.RemoteException; import android.os.SystemProperties; import android.provider.Settings.Global; import android.util.Slog; import com.android.internal.app.LocalePicker; import com.android.internal.app.LocalePicker.LocaleInfo; import com.android.internal.util.IndentingPrintWriter; import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Locale; /** * Represent a logical device of type Playback residing in Android system. */ final class HdmiCecLocalDevicePlayback extends HdmiCecLocalDevice { private static final String TAG = "HdmiCecLocalDevicePlayback"; private static final boolean WAKE_ON_HOTPLUG = SystemProperties.getBoolean(Constants.PROPERTY_WAKE_ON_HOTPLUG, true); private boolean mIsActiveSource = false; // Used to keep the device awake while it is the active source. For devices that // cannot wake up via CEC commands, this address the inconvenience of having to // turn them on. True by default, and can be disabled (i.e. device can go to sleep // in active device status) by explicitly setting the system property // persist.sys.hdmi.keep_awake to false. // Lazily initialized - should call getWakeLock() to get the instance. private ActiveWakeLock mWakeLock; // If true, turn off TV upon standby. False by default. private boolean mAutoTvOff; HdmiCecLocalDevicePlayback(HdmiControlService service) { super(service, HdmiDeviceInfo.DEVICE_PLAYBACK); mAutoTvOff = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, false); // The option is false by default. Update settings db as well to have the right // initial setting on UI. mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, mAutoTvOff); } @Override @ServiceThreadOnly protected void onAddressAllocated(int logicalAddress, int reason) { assertRunOnServiceThread(); mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( mAddress, mService.getPhysicalAddress(), mDeviceType)); mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( mAddress, mService.getVendorId())); startQueuedActions(); } @Override @ServiceThreadOnly protected int getPreferredAddress() { assertRunOnServiceThread(); return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK, Constants.ADDR_UNREGISTERED); } @Override @ServiceThreadOnly protected void setPreferredAddress(int addr) { assertRunOnServiceThread(); SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK, String.valueOf(addr)); } @ServiceThreadOnly void oneTouchPlay(IHdmiControlCallback callback) { assertRunOnServiceThread(); if (hasAction(OneTouchPlayAction.class)) { Slog.w(TAG, "oneTouchPlay already in progress"); invokeCallback(callback, HdmiControlManager.RESULT_ALREADY_IN_PROGRESS); return; } // TODO: Consider the case of multiple TV sets. For now we always direct the command // to the primary one. OneTouchPlayAction action = OneTouchPlayAction.create(this, Constants.ADDR_TV, callback); if (action == null) { Slog.w(TAG, "Cannot initiate oneTouchPlay"); invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION); return; } addAndStartAction(action); } @ServiceThreadOnly void queryDisplayStatus(IHdmiControlCallback callback) { assertRunOnServiceThread(); if (hasAction(DevicePowerStatusAction.class)) { Slog.w(TAG, "queryDisplayStatus already in progress"); invokeCallback(callback, HdmiControlManager.RESULT_ALREADY_IN_PROGRESS); return; } DevicePowerStatusAction action = DevicePowerStatusAction.create(this, Constants.ADDR_TV, callback); if (action == null) { Slog.w(TAG, "Cannot initiate queryDisplayStatus"); invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION); return; } addAndStartAction(action); } @ServiceThreadOnly private void invokeCallback(IHdmiControlCallback callback, int result) { assertRunOnServiceThread(); try { callback.onComplete(result); } catch (RemoteException e) { Slog.e(TAG, "Invoking callback failed:" + e); } } @Override @ServiceThreadOnly void onHotplug(int portId, boolean connected) { assertRunOnServiceThread(); mCecMessageCache.flushAll(); // We'll not clear mIsActiveSource on the hotplug event to pass CETC 11.2.2-2 ~ 3. if (WAKE_ON_HOTPLUG && connected && mService.isPowerStandbyOrTransient()) { mService.wakeUp(); } if (!connected) { getWakeLock().release(); } } @Override @ServiceThreadOnly protected void onStandby(boolean initiatedByCec, int standbyAction) { assertRunOnServiceThread(); if (!mService.isControlEnabled() || initiatedByCec) { return; } switch (standbyAction) { case HdmiControlService.STANDBY_SCREEN_OFF: if (mAutoTvOff) { mService.sendCecCommand( HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_TV)); } break; case HdmiControlService.STANDBY_SHUTDOWN: // ACTION_SHUTDOWN is taken as a signal to power off all the devices. mService.sendCecCommand( HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_BROADCAST)); break; } } @Override @ServiceThreadOnly void setAutoDeviceOff(boolean enabled) { assertRunOnServiceThread(); mAutoTvOff = enabled; } @ServiceThreadOnly void setActiveSource(boolean on) { assertRunOnServiceThread(); mIsActiveSource = on; if (on) { getWakeLock().acquire(); } else { getWakeLock().release(); } } @ServiceThreadOnly private ActiveWakeLock getWakeLock() { assertRunOnServiceThread(); if (mWakeLock == null) { if (SystemProperties.getBoolean(Constants.PROPERTY_KEEP_AWAKE, true)) { mWakeLock = new SystemWakeLock(); } else { // Create a dummy lock object that doesn't do anything about wake lock, // hence allows the device to go to sleep even if it's the active source. mWakeLock = new ActiveWakeLock() { @Override public void acquire() { } @Override public void release() { } @Override public boolean isHeld() { return false; } }; HdmiLogger.debug("No wakelock is used to keep the display on."); } } return mWakeLock; } @Override protected boolean canGoToStandby() { return !getWakeLock().isHeld(); } @Override @ServiceThreadOnly protected boolean handleActiveSource(HdmiCecMessage message) { assertRunOnServiceThread(); int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); mayResetActiveSource(physicalAddress); return true; // Broadcast message. } private void mayResetActiveSource(int physicalAddress) { if (physicalAddress != mService.getPhysicalAddress()) { setActiveSource(false); } } @ServiceThreadOnly protected boolean handleUserControlPressed(HdmiCecMessage message) { assertRunOnServiceThread(); wakeUpIfActiveSource(); return super.handleUserControlPressed(message); } @Override @ServiceThreadOnly protected boolean handleSetStreamPath(HdmiCecMessage message) { assertRunOnServiceThread(); int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); maySetActiveSource(physicalAddress); maySendActiveSource(message.getSource()); wakeUpIfActiveSource(); return true; // Broadcast message. } // Samsung model we tested sends and // in a row, and then changes the input to the internal source if there is no // in response. To handle this, we'll set ActiveSource aggressively. @Override @ServiceThreadOnly protected boolean handleRoutingChange(HdmiCecMessage message) { assertRunOnServiceThread(); int newPath = HdmiUtils.twoBytesToInt(message.getParams(), 2); maySetActiveSource(newPath); return true; // Broadcast message. } @Override @ServiceThreadOnly protected boolean handleRoutingInformation(HdmiCecMessage message) { assertRunOnServiceThread(); int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); maySetActiveSource(physicalAddress); return true; // Broadcast message. } private void maySetActiveSource(int physicalAddress) { setActiveSource(physicalAddress == mService.getPhysicalAddress()); } private void wakeUpIfActiveSource() { if (!mIsActiveSource) { return; } // Wake up the device if the power is in standby mode, or its screen is off - // which can happen if the device is holding a partial lock. if (mService.isPowerStandbyOrTransient() || !mService.getPowerManager().isScreenOn()) { mService.wakeUp(); } } private void maySendActiveSource(int dest) { if (mIsActiveSource) { mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource( mAddress, mService.getPhysicalAddress())); // Always reports menu-status active to receive RCP. mService.sendCecCommand(HdmiCecMessageBuilder.buildReportMenuStatus( mAddress, dest, Constants.MENU_STATE_ACTIVATED)); } } @Override @ServiceThreadOnly protected boolean handleRequestActiveSource(HdmiCecMessage message) { assertRunOnServiceThread(); maySendActiveSource(message.getSource()); return true; // Broadcast message. } @ServiceThreadOnly protected boolean handleSetMenuLanguage(HdmiCecMessage message) { assertRunOnServiceThread(); try { String iso3Language = new String(message.getParams(), 0, 3, "US-ASCII"); Locale currentLocale = mService.getContext().getResources().getConfiguration().locale; if (currentLocale.getISO3Language().equals(iso3Language)) { // Do not switch language if the new language is the same as the current one. // This helps avoid accidental country variant switching from en_US to en_AU // due to the limitation of CEC. See the warning below. return true; } // Don't use Locale.getAvailableLocales() since it returns a locale // which is not available on Settings. final List localeInfos = LocalePicker.getAllAssetLocales( mService.getContext(), false); for (LocaleInfo localeInfo : localeInfos) { if (localeInfo.getLocale().getISO3Language().equals(iso3Language)) { // WARNING: CEC adopts ISO/FDIS-2 for language code, while Android requires // additional country variant to pinpoint the locale. This keeps the right // locale from being chosen. 'eng' in the CEC command, for instance, // will always be mapped to en-AU among other variants like en-US, en-GB, // an en-IN, which may not be the expected one. LocalePicker.updateLocale(localeInfo.getLocale()); return true; } } Slog.w(TAG, "Can't handle of " + iso3Language); return false; } catch (UnsupportedEncodingException e) { return false; } } @Override @ServiceThreadOnly protected void sendStandby(int deviceId) { assertRunOnServiceThread(); // Playback device can send to TV only. Ignore the parameter. int targetAddress = Constants.ADDR_TV; mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress)); } @Override @ServiceThreadOnly protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { super.disableDevice(initiatedByCec, callback); assertRunOnServiceThread(); if (!initiatedByCec && mIsActiveSource) { mService.sendCecCommand(HdmiCecMessageBuilder.buildInactiveSource( mAddress, mService.getPhysicalAddress())); } setActiveSource(false); checkIfPendingActionsCleared(); } @Override protected void dump(final IndentingPrintWriter pw) { super.dump(pw); pw.println("mIsActiveSource: " + mIsActiveSource); pw.println("mAutoTvOff:" + mAutoTvOff); } // Wrapper interface over PowerManager.WakeLock private interface ActiveWakeLock { void acquire(); void release(); boolean isHeld(); } private class SystemWakeLock implements ActiveWakeLock { private final WakeLock mWakeLock; public SystemWakeLock() { mWakeLock = mService.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); mWakeLock.setReferenceCounted(false); } @Override public void acquire() { mWakeLock.acquire(); HdmiLogger.debug("active source: %b. Wake lock acquired", mIsActiveSource); } @Override public void release() { mWakeLock.release(); HdmiLogger.debug("Wake lock released"); } @Override public boolean isHeld() { return mWakeLock.isHeld(); } } }