diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 87cabc6aaa6c0..ee02883f0df76 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -155,6 +155,18 @@ abstract class HdmiCecLocalDevice { return handleSystemAudioModeStatus(message); case HdmiCec.MESSAGE_REPORT_AUDIO_STATUS: return handleReportAudioStatus(message); + case HdmiCec.MESSAGE_STANDBY: + return handleStandby(message); + case HdmiCec.MESSAGE_TEXT_VIEW_ON: + return handleTextViewOn(message); + case HdmiCec.MESSAGE_IMAGE_VIEW_ON: + return handleImageViewOn(message); + case HdmiCec.MESSAGE_USER_CONTROL_PRESSED: + return handleUserControlPressed(message); + case HdmiCec.MESSAGE_SET_STREAM_PATH: + return handleSetStreamPath(message); + case HdmiCec.MESSAGE_GIVE_DEVICE_POWER_STATUS: + return handleGiveDevicePowerStatus(message); default: return false; } @@ -275,6 +287,67 @@ abstract class HdmiCecLocalDevice { return false; } + @ServiceThreadOnly + protected boolean handleStandby(HdmiCecMessage message) { + assertRunOnServiceThread(); + // Seq #12 + if (mService.isControlEnabled() && !isInPresetInstallationMode() + && mService.isPowerOnOrTransient()) { + mService.standby(); + return true; + } + return false; + } + + @ServiceThreadOnly + protected boolean handleUserControlPressed(HdmiCecMessage message) { + assertRunOnServiceThread(); + final int opCode = message.getOpcode(); + final byte[] params = message.getParams(); + if (mService.isPowerOnOrTransient() && isPowerOffOrToggleCommand(message)) { + mService.standby(); + return true; + } else if (mService.isPowerStandbyOrTransient() && isPowerOnOrToggleCommand(message)) { + mService.wakeUp(); + return true; + } + return false; + } + + private static boolean isPowerOnOrToggleCommand(HdmiCecMessage message) { + byte[] params = message.getParams(); + return message.getOpcode() == HdmiCec.MESSAGE_USER_CONTROL_PRESSED && params.length == 1 + && (params[0] == HdmiConstants.UI_COMMAND_POWER + || params[0] == HdmiConstants.UI_COMMAND_POWER_ON_FUNCTION + || params[0] == HdmiConstants.UI_COMMAND_POWER_TOGGLE_FUNCTION); + } + + private static boolean isPowerOffOrToggleCommand(HdmiCecMessage message) { + byte[] params = message.getParams(); + return message.getOpcode() == HdmiCec.MESSAGE_USER_CONTROL_PRESSED && params.length == 1 + && (params[0] == HdmiConstants.UI_COMMAND_POWER + || params[0] == HdmiConstants.UI_COMMAND_POWER_OFF_FUNCTION + || params[0] == HdmiConstants.UI_COMMAND_POWER_TOGGLE_FUNCTION); + } + + protected boolean handleTextViewOn(HdmiCecMessage message) { + return false; + } + + protected boolean handleImageViewOn(HdmiCecMessage message) { + return false; + } + + protected boolean handleSetStreamPath(HdmiCecMessage message) { + return false; + } + + protected boolean handleGiveDevicePowerStatus(HdmiCecMessage message) { + mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPowerStatus( + mAddress, message.getSource(), mService.getPowerStatus())); + return true; + } + @ServiceThreadOnly final void handleAddressAllocated(int logicalAddress) { assertRunOnServiceThread(); @@ -323,6 +396,10 @@ abstract class HdmiCecLocalDevice { @ServiceThreadOnly void addAndStartAction(final FeatureAction action) { assertRunOnServiceThread(); + if (mService.isPowerStandbyOrTransient()) { + Slog.w(TAG, "Skip the action during Standby: " + action); + return; + } mActions.add(action); action.start(); } @@ -361,6 +438,7 @@ abstract class HdmiCecLocalDevice { void removeAction(final FeatureAction action) { assertRunOnServiceThread(); mActions.remove(action); + checkIfPendingActionsCleared(); } // Remove all actions matched with the given Class type. @@ -383,8 +461,14 @@ abstract class HdmiCecLocalDevice { mActions.remove(action); } } + checkIfPendingActionsCleared(); } + protected void checkIfPendingActionsCleared() { + if (mActions.isEmpty()) { + mService.onPendingActionsCleared(); + } + } protected void assertRunOnServiceThread() { if (Looper.myLooper() != mService.getServiceLooper()) { throw new IllegalStateException("Should run on service thread."); @@ -488,4 +572,23 @@ abstract class HdmiCecLocalDevice { assertRunOnServiceThread(); return mService.pathToPortId(newPath); } + + /** + * Called when the system started transition to standby mode. + * + * @param initiatedByCec true if this power sequence is initiated + * by the reception the CEC messages like + */ + protected void onTransitionToStandby(boolean initiatedByCec) { + // If there are no outstanding actions, we'll go to STANDBY state. + checkIfPendingActionsCleared(); + } + + /** + * Called when the system goes to standby mode. + * + * @param initiatedByCec true if this power sequence is initiated + * by the reception the CEC messages like + */ + protected void onStandBy(boolean initiatedByCec) {} } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java index d7e36b37d00bd..a631172447f1e 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java @@ -17,6 +17,7 @@ package com.android.server.hdmi; import android.hardware.hdmi.HdmiCec; +import android.hardware.hdmi.HdmiCecMessage; import android.hardware.hdmi.IHdmiControlCallback; import android.os.RemoteException; import android.util.Slog; @@ -29,6 +30,8 @@ import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; final class HdmiCecLocalDevicePlayback extends HdmiCecLocalDevice { private static final String TAG = "HdmiCecLocalDevicePlayback"; + private boolean mIsActiveSource = false; + HdmiCecLocalDevicePlayback(HdmiControlService service) { super(service, HdmiCec.DEVICE_PLAYBACK); } @@ -93,7 +96,57 @@ final class HdmiCecLocalDevicePlayback extends HdmiCecLocalDevice { @ServiceThreadOnly void onHotplug(int portId, boolean connected) { assertRunOnServiceThread(); - // TODO: clear devices connected to the given port id. mCecMessageCache.flushAll(); + mIsActiveSource = false; + if (connected && mService.isPowerStandbyOrTransient()) { + mService.wakeUp(); + } + } + + @ServiceThreadOnly + void markActiveSource() { + assertRunOnServiceThread(); + mIsActiveSource = true; + } + + @Override + @ServiceThreadOnly + protected boolean handleActiveSource(HdmiCecMessage message) { + assertRunOnServiceThread(); + int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); + if (physicalAddress != mService.getPhysicalAddress()) { + mIsActiveSource = false; + if (mService.isPowerOnOrTransient()) { + mService.standby(); + } + return true; + } + return false; + } + + @Override + @ServiceThreadOnly + protected boolean handleSetStreamPath(HdmiCecMessage message) { + assertRunOnServiceThread(); + int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); + if (physicalAddress == mService.getPhysicalAddress()) { + if (mService.isPowerStandbyOrTransient()) { + mService.wakeUp(); + } + return true; + } + return false; + } + + @Override + @ServiceThreadOnly + protected void onTransitionToStandby(boolean initiatedByCec) { + assertRunOnServiceThread(); + if (!initiatedByCec && mIsActiveSource) { + mService.sendCecCommand(HdmiCecMessageBuilder.buildInactiveSource( + mAddress, mService.getPhysicalAddress())); + } + mIsActiveSource = false; + checkIfPendingActionsCleared(); } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 72d44fad837bc..868571ed8ecdf 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -441,6 +441,26 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { return true; } + @Override + @ServiceThreadOnly + protected boolean handleTextViewOn(HdmiCecMessage message) { + assertRunOnServiceThread(); + if (mService.isPowerStandbyOrTransient()) { + mService.wakeUp(); + } + // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel + // that represents the source device. + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleImageViewOn(HdmiCecMessage message) { + assertRunOnServiceThread(); + // Currently, it's the same as . + return handleTextViewOn(message); + } + @ServiceThreadOnly private void launchDeviceDiscovery() { assertRunOnServiceThread(); @@ -985,4 +1005,42 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { assertRunOnServiceThread(); mAutoDeviceOff = enabled; } + + @Override + @ServiceThreadOnly + protected void onTransitionToStandby(boolean initiatedByCec) { + assertRunOnServiceThread(); + // Remove any repeated working actions. + // HotplugDetectionAction will be reinstated during the wake up process. + // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> + // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). + removeAction(HotplugDetectionAction.class); + checkIfPendingActionsCleared(); + } + + @Override + @ServiceThreadOnly + protected void onStandBy(boolean initiatedByCec) { + assertRunOnServiceThread(); + // Seq #11 + if (!mService.isControlEnabled()) { + return; + } + if (!initiatedByCec) { + mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( + mAddress, HdmiCec.ADDR_BROADCAST)); + } + } + + @Override + @ServiceThreadOnly + protected boolean handleStandby(HdmiCecMessage message) { + assertRunOnServiceThread(); + // Seq #12 + // Tv accepts directly addressed only. + if (message.getDestination() == mAddress) { + super.handleStandby(message); + } + return false; + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java index 361a063b729d5..b1ea1349be3b7 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java @@ -269,6 +269,18 @@ public class HdmiCecMessageBuilder { physicalAddressToParam(physicalAddress)); } + /** + * Build <Inactive Source> command. + * + * @param src source address of command + * @param physicalAddress physical address of the device to become inactive + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildInactiveSource(int src, int physicalAddress) { + return buildCommand(src, HdmiCec.ADDR_BROADCAST, HdmiCec.MESSAGE_INACTIVE_SOURCE, + physicalAddressToParam(physicalAddress)); + } + /** * Build <Set Stream Path> command. * @@ -312,6 +324,21 @@ public class HdmiCecMessageBuilder { return buildCommand(src, dest, HdmiCec.MESSAGE_GIVE_DEVICE_POWER_STATUS); } + /** + * Build <Report Power Status> command. + * + * @param src source address of command + * @param dest destination address of command + * @param powerStatus power status of the device + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildReportPowerStatus(int src, int dest, int powerStatus) { + byte[] param = new byte[] { + (byte) (powerStatus) + }; + return buildCommand(src, dest, HdmiCec.MESSAGE_REPORT_POWER_STATUS, param); + } + /** * Build <System Audio Mode Request> command. * @@ -388,6 +415,17 @@ public class HdmiCecMessageBuilder { return buildCommand(src, dest, HdmiCec.MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS); } + /** + * Build <Standby> command. + * + * @param src source address of command + * @param dest destination address of command + * @return newly created {@link HdmiCecMessage} + */ + public static HdmiCecMessage buildStandby(int src, int dest) { + return buildCommand(src, dest, HdmiCec.MESSAGE_STANDBY); + } + /***** Please ADD new buildXXX() methods above. ******/ /** diff --git a/services/core/java/com/android/server/hdmi/HdmiConstants.java b/services/core/java/com/android/server/hdmi/HdmiConstants.java index ab5b8d84487b5..c0accc3d6e383 100644 --- a/services/core/java/com/android/server/hdmi/HdmiConstants.java +++ b/services/core/java/com/android/server/hdmi/HdmiConstants.java @@ -45,6 +45,8 @@ final class HdmiConstants { static final int UI_COMMAND_MUTE = 0x43; static final int UI_COMMAND_MUTE_FUNCTION = 0x65; static final int UI_COMMAND_RESTORE_VOLUME_FUNCTION = 0x66; + static final int UI_COMMAND_POWER_TOGGLE_FUNCTION = 0x6B; + static final int UI_COMMAND_POWER_OFF_FUNCTION = 0x6C; static final int UI_COMMAND_POWER_ON_FUNCTION = 0x6D; // Bit mask used to get the routing path of the top level device. diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index a2df85250b437..9ef818d6e58aa 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -17,7 +17,10 @@ package com.android.server.hdmi; import android.annotation.Nullable; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.hardware.hdmi.HdmiCec; import android.hardware.hdmi.HdmiCecDeviceInfo; import android.hardware.hdmi.HdmiCecMessage; @@ -34,8 +37,9 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; +import android.os.PowerManager; import android.os.RemoteException; -import android.util.Log; +import android.os.SystemClock; import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; @@ -86,6 +90,24 @@ public final class HdmiControlService extends SystemService { void onPollingFinished(List ackedAddress); } + private class PowerStateReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case Intent.ACTION_SCREEN_OFF: + if (isPowerOnOrTransient()) { + onStandby(); + } + break; + case Intent.ACTION_SCREEN_ON: + if (isPowerStandbyOrTransient()) { + onWakeUp(); + } + break; + } + } + } + // A thread to handle synchronous IO of CEC and MHL control service. // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) // and sparse call it shares a thread to handle IO operations. @@ -143,6 +165,14 @@ public final class HdmiControlService extends SystemService { // from being modified. private List mPortInfo; + private final PowerStateReceiver mPowerStateReceiver = new PowerStateReceiver(); + + @ServiceThreadOnly + private int mPowerStatus = HdmiCec.POWER_STATUS_STANDBY; + + @ServiceThreadOnly + private boolean mStandbyMessageReceived = false; + public HdmiControlService(Context context) { super(context); mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray( @@ -152,9 +182,11 @@ public final class HdmiControlService extends SystemService { @Override public void onStart() { mIoThread.start(); + mPowerStatus = HdmiCec.POWER_STATUS_TRANSIENT_TO_ON; mCecController = HdmiCecController.create(this); if (mCecController != null) { + mCecController.setOption(HdmiCec.OPTION_CEC_SERVICE_CONTROL, HdmiCec.DISABLED); initializeLocalDevices(mLocalDevices); } else { Slog.i(TAG, "Device does not support HDMI-CEC."); @@ -167,6 +199,14 @@ public final class HdmiControlService extends SystemService { mPortInfo = initPortInfo(); publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); + // Register broadcast receiver for power state change. + if (mCecController != null || mMhlController != null) { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_SCREEN_ON); + getContext().registerReceiver(mPowerStateReceiver, filter); + } + // TODO: Read the preference for SystemAudioMode and initialize mSystemAudioMode and // start to monitor the preference value and invoke SystemAudioActionFromTv if needed. mHdmiControlEnabled = true; @@ -199,6 +239,9 @@ public final class HdmiControlService extends SystemService { // Address allocation completed for all devices. Notify each device. if (deviceTypes.size() == finished.size()) { + if (mPowerStatus == HdmiCec.POWER_STATUS_TRANSIENT_TO_ON) { + mPowerStatus = HdmiCec.POWER_STATUS_ON; + } notifyAddressAllocated(devices); } } @@ -531,7 +574,7 @@ public final class HdmiControlService extends SystemService { } private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient { - private IHdmiSystemAudioModeChangeListener mListener; + private final IHdmiSystemAudioModeChangeListener mListener; public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) { mListener = listener; @@ -795,7 +838,7 @@ public final class HdmiControlService extends SystemService { public void run() { HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { - Log.w(TAG, "Local tv device not available to change arc mode."); + Slog.w(TAG, "Local tv device not available to change arc mode."); return; } } @@ -992,4 +1035,82 @@ public final class HdmiControlService extends SystemService { return mHdmiControlEnabled; } } + + int getPowerStatus() { + return mPowerStatus; + } + + boolean isPowerOnOrTransient() { + return mPowerStatus == HdmiCec.POWER_STATUS_ON + || mPowerStatus == HdmiCec.POWER_STATUS_TRANSIENT_TO_ON; + } + + boolean isPowerStandbyOrTransient() { + return mPowerStatus == HdmiCec.POWER_STATUS_STANDBY + || mPowerStatus == HdmiCec.POWER_STATUS_TRANSIENT_TO_STANDBY; + } + + boolean isPowerStandby() { + return mPowerStatus == HdmiCec.POWER_STATUS_STANDBY; + } + + @ServiceThreadOnly + void wakeUp() { + assertRunOnServiceThread(); + PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); + pm.wakeUp(SystemClock.uptimeMillis()); + // PowerManger will send the broadcast Intent.ACTION_SCREEN_ON and after this gets + // the intent, the sequence will continue at onWakeUp(). + } + + @ServiceThreadOnly + void standby() { + assertRunOnServiceThread(); + mStandbyMessageReceived = true; + PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); + pm.goToSleep(SystemClock.uptimeMillis()); + // PowerManger will send the broadcast Intent.ACTION_SCREEN_OFF and after this gets + // the intent, the sequence will continue at onStandby(). + } + + @ServiceThreadOnly + private void onWakeUp() { + assertRunOnServiceThread(); + mPowerStatus = HdmiCec.POWER_STATUS_TRANSIENT_TO_ON; + if (mCecController != null) { + mCecController.setOption(HdmiCec.OPTION_CEC_SERVICE_CONTROL, HdmiCec.ENABLED); + initializeLocalDevices(mLocalDevices); + } else { + Slog.i(TAG, "Device does not support HDMI-CEC."); + } + // TODO: Initialize MHL local devices. + } + + @ServiceThreadOnly + private void onStandby() { + assertRunOnServiceThread(); + mPowerStatus = HdmiCec.POWER_STATUS_TRANSIENT_TO_STANDBY; + for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { + device.onTransitionToStandby(mStandbyMessageReceived); + } + } + + /** + * Called when there are the outstanding actions in the local devices. + * This callback is used to wait for when the action queue is empty + * during the power state transition to standby. + */ + @ServiceThreadOnly + void onPendingActionsCleared() { + assertRunOnServiceThread(); + if (mPowerStatus != HdmiCec.POWER_STATUS_TRANSIENT_TO_STANDBY) { + return; + } + mPowerStatus = HdmiCec.POWER_STATUS_STANDBY; + for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { + device.onStandBy(mStandbyMessageReceived); + } + mStandbyMessageReceived = false; + mCecController.setOption(HdmiCec.OPTION_CEC_SERVICE_CONTROL, HdmiCec.DISABLED); + } } diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java index e0a3a8b031814..ebb906a5c2163 100644 --- a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java +++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java @@ -15,9 +15,10 @@ */ package com.android.server.hdmi; -import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.hdmi.HdmiCec; import android.hardware.hdmi.HdmiCecMessage; +import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.IHdmiControlCallback; import android.os.RemoteException; import android.util.Slog; @@ -54,7 +55,7 @@ final class OneTouchPlayAction extends FeatureAction { private int mPowerStatusCounter = 0; // Factory method. Ensures arguments are valid. - static OneTouchPlayAction create(HdmiCecLocalDevice source, + static OneTouchPlayAction create(HdmiCecLocalDevicePlayback source, int targetAddress, IHdmiControlCallback callback) { if (source == null || callback == null) { Slog.e(TAG, "Wrong arguments"); @@ -83,6 +84,8 @@ final class OneTouchPlayAction extends FeatureAction { private void broadcastActiveSource() { sendCommand(HdmiCecMessageBuilder.buildActiveSource(getSourceAddress(), getSourcePath())); + // Because only playback device can create this action, it's safe to cast. + playback().markActiveSource(); } private void queryDevicePowerStatus() {