/*
|
* 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()));
|
|
// wakup tv on cec initialize
|
mService.sendCecCommand(HdmiCecMessageBuilder.buildImageViewOn(mAddress, Constants.ADDR_TV));
|
mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, mService.getPhysicalAddress()));
|
|
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_BROADCAST));
|
mService.sendCecCommand(
|
HdmiCecMessageBuilder.buildInactiveSource(mAddress,
|
mService.getPhysicalAddress()));
|
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));
|
mService.sendCecCommand(
|
HdmiCecMessageBuilder.buildInactiveSource(mAddress,
|
mService.getPhysicalAddress()));
|
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.isControlEnabled()) {
|
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();
|
}
|
}
|
}
|