| /* |
| * 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.hardware.tv.cec.V1_0.SendMessageResult; |
| import android.os.PowerManager; |
| import android.os.PowerManager.WakeLock; |
| import android.os.SystemProperties; |
| import android.provider.Settings.Global; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| 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 com.android.server.hdmi.HdmiControlService.SendMessageCallback; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** |
| * Represent a logical device of type Playback residing in Android system. |
| */ |
| public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource { |
| private static final String TAG = "HdmiCecLocalDevicePlayback"; |
| |
| private static final boolean WAKE_ON_HOTPLUG = |
| SystemProperties.getBoolean(Constants.PROPERTY_WAKE_ON_HOTPLUG, true); |
| |
| private static final boolean SET_MENU_LANGUAGE = |
| SystemProperties.getBoolean(Constants.PROPERTY_SET_MENU_LANGUAGE, 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; |
| |
| // Local active port number used for Routing Control. |
| // Default 0 means HOME is the current active path. Temp solution only. |
| // TODO(amyjojo): adding system constants for input ports to TIF mapping. |
| private int mLocalActivePath = 0; |
| |
| 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(); |
| if (reason == mService.INITIATED_BY_ENABLE_CEC) { |
| mService.setAndBroadcastActiveSource(mService.getPhysicalAddress(), |
| getDeviceInfo().getDeviceType(), Constants.ADDR_BROADCAST); |
| } |
| mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( |
| mAddress, mService.getPhysicalAddress(), mDeviceType)); |
| mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( |
| mAddress, mService.getVendorId())); |
| if (mService.audioSystem() == null) { |
| // If current device is not a functional audio system device, |
| // send message to potential audio system device in the system to get the system |
| // audio mode status. If no response, set to false. |
| mService.sendCecCommand(HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( |
| mAddress, Constants.ADDR_AUDIO_SYSTEM), new SendMessageCallback() { |
| @Override |
| public void onSendCompleted(int error) { |
| if (error != SendMessageResult.SUCCESS) { |
| HdmiLogger.debug( |
| "AVR did not respond to <Give System Audio Mode Status>"); |
| mService.setSystemAudioActivated(false); |
| } |
| } |
| }); |
| } |
| 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(); |
| mService.writeStringSystemProperty(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK, |
| String.valueOf(addr)); |
| } |
| |
| @ServiceThreadOnly |
| void queryDisplayStatus(IHdmiControlCallback callback) { |
| assertRunOnServiceThread(); |
| List<DevicePowerStatusAction> actions = getActions(DevicePowerStatusAction.class); |
| if (!actions.isEmpty()) { |
| Slog.i(TAG, "queryDisplayStatus already in progress"); |
| actions.get(0).addCallback(callback); |
| 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); |
| } |
| |
| @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 || !mAutoTvOff) { |
| return; |
| } |
| switch (standbyAction) { |
| case HdmiControlService.STANDBY_SCREEN_OFF: |
| 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 |
| @VisibleForTesting |
| void setIsActiveSource(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(); |
| } |
| |
| @ServiceThreadOnly |
| protected boolean handleUserControlPressed(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| wakeUpIfActiveSource(); |
| return super.handleUserControlPressed(message); |
| } |
| |
| @Override |
| protected 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(); |
| } |
| } |
| |
| @Override |
| protected 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)); |
| } |
| } |
| |
| @ServiceThreadOnly |
| protected boolean handleSetMenuLanguage(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| if (!SET_MENU_LANGUAGE) { |
| return false; |
| } |
| |
| 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<LocaleInfo> 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 <Set Menu Language> of " + iso3Language); |
| return false; |
| } catch (UnsupportedEncodingException e) { |
| Slog.w(TAG, "Can't handle <Set Menu Language>", e); |
| return false; |
| } |
| } |
| |
| @Override |
| protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { |
| // System Audio Mode only turns on/off when Audio System broadcasts on/off message. |
| // For device with type 4 and 5, it can set system audio mode on/off |
| // when there is another audio system device connected into the system first. |
| if (message.getDestination() != Constants.ADDR_BROADCAST |
| || message.getSource() != Constants.ADDR_AUDIO_SYSTEM |
| || mService.audioSystem() != null) { |
| return true; |
| } |
| boolean setSystemAudioModeOn = HdmiUtils.parseCommandParamSystemAudioStatus(message); |
| if (mService.isSystemAudioActivated() != setSystemAudioModeOn) { |
| mService.setSystemAudioActivated(setSystemAudioModeOn); |
| } |
| return true; |
| } |
| |
| @Override |
| protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { |
| // Only directly addressed System Audio Mode Status message can change internal |
| // system audio mode status. |
| if (message.getDestination() == mAddress |
| && message.getSource() == Constants.ADDR_AUDIO_SYSTEM) { |
| boolean setSystemAudioModeOn = HdmiUtils.parseCommandParamSystemAudioStatus(message); |
| if (mService.isSystemAudioActivated() != setSystemAudioModeOn) { |
| mService.setSystemAudioActivated(setSystemAudioModeOn); |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| protected int findKeyReceiverAddress() { |
| return Constants.ADDR_TV; |
| } |
| |
| @Override |
| protected int findAudioReceiverAddress() { |
| if (mService.isSystemAudioActivated()) { |
| return Constants.ADDR_AUDIO_SYSTEM; |
| } |
| return Constants.ADDR_TV; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { |
| super.disableDevice(initiatedByCec, callback); |
| |
| assertRunOnServiceThread(); |
| if (!initiatedByCec && mIsActiveSource) { |
| mService.sendCecCommand(HdmiCecMessageBuilder.buildInactiveSource( |
| mAddress, mService.getPhysicalAddress())); |
| } |
| setIsActiveSource(false); |
| checkIfPendingActionsCleared(); |
| } |
| |
| private void routeToPort(int portId) { |
| // TODO(AMYJOJO): route to specific input of the port |
| mLocalActivePath = portId; |
| } |
| |
| @VisibleForTesting |
| protected int getLocalActivePath() { |
| return mLocalActivePath; |
| } |
| |
| @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(); |
| } |
| } |
| } |