Merge "Handle the power state change."
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 87cabc6..ee02883 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 @@
                 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;
         }
@@ -276,6 +288,67 @@
     }
 
     @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();
         mAddress = mPreferredAddress = logicalAddress;
@@ -323,6 +396,10 @@
     @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 @@
     void removeAction(final FeatureAction action) {
         assertRunOnServiceThread();
         mActions.remove(action);
+        checkIfPendingActionsCleared();
     }
 
     // Remove all actions matched with the given Class type.
@@ -383,8 +461,14 @@
                 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 @@
         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 <StandBy>
+     */
+    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 <StandBy>
+     */
+    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 d7e36b3..a631172 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 @@
 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 @@
     @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 72d44fa..868571e 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 @@
         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 <Text View On>.
+        return handleTextViewOn(message);
+    }
+
     @ServiceThreadOnly
     private void launchDeviceDiscovery() {
         assertRunOnServiceThread();
@@ -985,4 +1005,42 @@
         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 <Standby> 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 361a063..b1ea134 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
@@ -270,6 +270,18 @@
     }
 
     /**
+     * Build &lt;Inactive Source&gt; 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 &lt;Set Stream Path&gt; command.
      *
      * <p>This is a broadcast message sent to all devices on the bus.
@@ -313,6 +325,21 @@
     }
 
     /**
+     * Build &lt;Report Power Status&gt; 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 &lt;System Audio Mode Request&gt; command.
      *
      * @param src source address of command
@@ -388,6 +415,17 @@
         return buildCommand(src, dest, HdmiCec.MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS);
     }
 
+    /**
+     * Build &lt;Standby&gt; 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 ab5b8d8..c0accc3 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 @@
     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 63df9a2..95d8333 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.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 @@
         void onPollingFinished(List<Integer> 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 @@
     // from being modified.
     private List<HdmiPortInfo> 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 @@
     @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 @@
         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 @@
 
                     // 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);
                     }
                 }
@@ -534,7 +577,7 @@
     }
 
     private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient {
-        private IHdmiSystemAudioModeChangeListener mListener;
+        private final IHdmiSystemAudioModeChangeListener mListener;
 
         public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) {
             mListener = listener;
@@ -798,7 +841,7 @@
                 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;
                     }
                 }
@@ -995,4 +1038,82 @@
             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 e0a3a8b..ebb906a 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 @@
     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 @@
 
     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() {