Merge "CEC: Revamp volume control action." into lmp-dev
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index 4b5ced9..30f3576 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -55,10 +55,29 @@
     public static final int OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT = 1;
 
     /**
+     * Message used by TV to receive volume status from Audio Receiver. It should check volume value
+     * that is retrieved from extra value with the key {@link #EXTRA_MESSAGE_EXTRAM_PARAM1}. If the
+     * value is in range of [0,100], it is current volume of Audio Receiver. And there is another
+     * value, {@link #AVR_VOLUME_MUTED}, which is used to inform volume mute.
+     */
+    public static final int OSD_MESSAGE_AVR_VOLUME_CHANGED = 2;
+
+    /**
      * Used as an extra field in the intent {@link #ACTION_OSD_MESSAGE}. Contains the ID of
      * the message to display on screen.
      */
     public static final String EXTRA_MESSAGE_ID = "android.hardware.hdmi.extra.MESSAGE_ID";
+    /**
+     * Used as an extra field in the intent {@link #ACTION_OSD_MESSAGE}. Contains the extra value
+     * of the message.
+     */
+    public static final String EXTRA_MESSAGE_EXTRAM_PARAM1 =
+            "android.hardware.hdmi.extra.MESSAGE_EXTRA_PARAM1";
+
+    /**
+     * Volume value for mute state.
+     */
+    public static final int AVR_VOLUME_MUTED = 101;
 
     public static final int POWER_STATUS_UNKNOWN = -1;
     public static final int POWER_STATUS_ON = 0;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index bb22b4d..c5a6dbd 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -31,8 +31,6 @@
 
 import libcore.util.EmptyArray;
 
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -102,11 +100,6 @@
     // Stores the local CEC devices in the system. Device type is used for key.
     private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();
 
-    @IoThreadOnly
-    private final HdmiLogger mIoThreadLogger = new HdmiLogger(TAG);
-    @ServiceThreadOnly
-    private final HdmiLogger mServiceThreadLogger = new HdmiLogger(TAG);
-
     // Private constructor.  Use HdmiCecController.create().
     private HdmiCecController(HdmiControlService service) {
         mService = service;
@@ -210,9 +203,8 @@
         }
 
         final int assignedAddress = logicalAddress;
-        mIoThreadLogger.debug(
-                String.format("New logical address for device [%d]: [preferred:%d, assigned:%d]",
-                        deviceType, preferredAddress, assignedAddress));
+        HdmiLogger.debug("New logical address for device [%d]: [preferred:%d, assigned:%d]",
+                        deviceType, preferredAddress, assignedAddress);
         if (callback != null) {
             runOnServiceThread(new Runnable() {
                 @Override
@@ -449,7 +441,7 @@
                         allocated.add(address);
                     }
                 }
-                mIoThreadLogger.debug("[P]:Allocated Address=" + allocated);
+                HdmiLogger.debug("[P]:Allocated Address=" + allocated);
                 if (callback != null) {
                     runOnServiceThread(new Runnable() {
                         @Override
@@ -551,7 +543,7 @@
         runOnIoThread(new Runnable() {
             @Override
             public void run() {
-                mIoThreadLogger.debug("[S]:" + cecMessage);
+                HdmiLogger.debug("[S]:" + cecMessage);
                 byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
                 int i = 0;
                 int errorCode = Constants.SEND_RESULT_SUCCESS;
@@ -586,7 +578,7 @@
     private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
         assertRunOnServiceThread();
         HdmiCecMessage command = HdmiCecMessageBuilder.of(srcAddress, dstAddress, body);
-        mServiceThreadLogger.debug("[R]:" + command);
+        HdmiLogger.debug("[R]:" + command);
         onReceiveCommand(command);
     }
 
@@ -596,8 +588,7 @@
     @ServiceThreadOnly
     private void handleHotplug(int port, boolean connected) {
         assertRunOnServiceThread();
-        mServiceThreadLogger.debug(
-                "Hotplug event:[port:" + port + " , connected:" + connected + "]");
+        HdmiLogger.debug("Hotplug event:[port:%d, connected:%b]", port, connected);
         mService.onHotplug(port, connected);
     }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java b/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java
index 85f5be2..b2300a6 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java
@@ -43,9 +43,6 @@
  */
 abstract class HdmiCecFeatureAction {
     private static final String TAG = "HdmiCecFeatureAction";
-    // As all actions run in the same thread (service thread), it's fine to have single logger.
-    // TODO: create global logger for each threads and use them.
-    protected static final HdmiLogger DLOGGER = new HdmiLogger(TAG);
 
     // Timer handler message used for timeout event
     protected static final int MSG_TIMEOUT = 100;
@@ -264,10 +261,7 @@
     }
 
     protected final void sendUserControlPressedAndReleased(int targetAddress, int uiCommand) {
-        sendCommand(HdmiCecMessageBuilder.buildUserControlPressed(
-                getSourceAddress(), targetAddress, uiCommand));
-        sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(
-                getSourceAddress(), targetAddress));
+        mSource.sendUserControlPressedAndReleased(targetAddress, uiCommand);
     }
 
     protected final void addOnFinishedCallback(HdmiCecFeatureAction action, Runnable runnable) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index bc6a299..04e38dc 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -810,6 +810,13 @@
         Slog.w(TAG, "sendKeyEvent not implemented");
     }
 
+    void sendUserControlPressedAndReleased(int targetAddress, int cecKeycode) {
+        mService.sendCecCommand(HdmiCecMessageBuilder.buildUserControlPressed(
+                mAddress, targetAddress, cecKeycode));
+        mService.sendCecCommand(HdmiCecMessageBuilder.buildUserControlReleased(
+                mAddress, targetAddress));
+    }
+
     /**
      * Dump internal status of HdmiCecLocalDevice object.
      */
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 7ae2198..5994c76 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -120,8 +120,6 @@
     // other CEC devices since they might not have logical address.
     private final ArraySet<Integer> mCecSwitches = new ArraySet<Integer>();
 
-    private final HdmiLogger mSafeLogger = new HdmiLogger(TAG);
-
     HdmiCecLocalDeviceTv(HdmiControlService service) {
         super(service, HdmiDeviceInfo.DEVICE_TV);
         mPrevPortId = Constants.INVALID_PORT_ID;
@@ -700,8 +698,7 @@
 
     // # Seq 25
     void setSystemAudioMode(boolean on, boolean updateSetting) {
-        mSafeLogger.debug(String.format("System Audio Mode change[old:%b new:%b]",
-                mSystemAudioActivated, on));
+        HdmiLogger.debug("System Audio Mode change[old:%b new:%b]", mSystemAudioActivated, on);
 
         if (updateSetting) {
             mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
@@ -832,6 +829,8 @@
                     AudioManager.STREAM_MUSIC);
             mService.setAudioStatus(mute,
                     VolumeControlAction.scaleToCustomVolume(volume, maxVolume));
+            displayOsd(HdmiControlManager.OSD_MESSAGE_AVR_VOLUME_CHANGED,
+                    mute ? HdmiControlManager.AVR_VOLUME_MUTED : volume);
         }
     }
 
@@ -855,12 +854,13 @@
             }
         }
 
-        // Remove existing volume action.
-        removeAction(VolumeControlAction.class);
-
-        HdmiDeviceInfo avr = getAvrDeviceInfo();
-        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
-                cecVolume, delta > 0));
+        List<VolumeControlAction> actions = getActions(VolumeControlAction.class);
+        if (actions.isEmpty()) {
+            addAndStartAction(new VolumeControlAction(this,
+                    getAvrDeviceInfo().getLogicalAddress(), delta > 0));
+        } else {
+            actions.get(0).handleVolumeChange(delta > 0);
+        }
     }
 
     @ServiceThreadOnly
@@ -872,8 +872,9 @@
 
         // Remove existing volume action.
         removeAction(VolumeControlAction.class);
-        HdmiDeviceInfo avr = getAvrDeviceInfo();
-        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
+        sendUserControlPressedAndReleased(getAvrDeviceInfo().getLogicalAddress(),
+                mute ? HdmiCecKeycode.CEC_KEYCODE_MUTE_FUNCTION :
+                        HdmiCecKeycode.CEC_KEYCODE_RESTORE_VOLUME_FUNCTION);
     }
 
     @Override
@@ -935,7 +936,7 @@
     protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
         assertRunOnServiceThread();
         if (!isMessageForSystemAudio(message)) {
-            mSafeLogger.warning("Invalid <Set System Audio Mode> message:" + message);
+            HdmiLogger.warning("Invalid <Set System Audio Mode> message:" + message);
             return false;
         }
         SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
@@ -949,7 +950,7 @@
     protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
         assertRunOnServiceThread();
         if (!isMessageForSystemAudio(message)) {
-            mSafeLogger.warning("Invalid <System Audio Mode Status> message:" + message);
+            HdmiLogger.warning("Invalid <System Audio Mode Status> message:" + message);
             return false;
         }
         setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
@@ -1445,6 +1446,12 @@
         mService.displayOsd(messageId);
     }
 
+    @ServiceThreadOnly
+    void displayOsd(int messageId, int extra) {
+        assertRunOnServiceThread();
+        mService.displayOsd(messageId, extra);
+    }
+
     // Seq #54 and #55
     @ServiceThreadOnly
     void startOneTouchRecord(int recorderAddress, byte[] recordSource) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
index 8b345cf..0b3d9fb 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
@@ -51,7 +51,6 @@
     }
 
     final SparseArray<ValidationInfo> mValidationInfo = new SparseArray<>();
-    private final HdmiLogger mSpamSafeLogger = new HdmiLogger(TAG);
 
     public HdmiCecMessageValidator(HdmiControlService service) {
         mService = service;
@@ -183,32 +182,32 @@
         int opcode = message.getOpcode();
         ValidationInfo info = mValidationInfo.get(opcode);
         if (info == null) {
-            mSpamSafeLogger.warning("No validation information for the message: " + message);
+            HdmiLogger.warning("No validation information for the message: " + message);
             return true;
         }
 
         // Check the source field.
         if (message.getSource() == Constants.ADDR_UNREGISTERED &&
                 (info.addressType & SRC_UNREGISTERED) == 0) {
-            mSpamSafeLogger.warning("Unexpected source: " + message);
+            HdmiLogger.warning("Unexpected source: " + message);
             return false;
         }
         // Check the destination field.
         if (message.getDestination() == Constants.ADDR_BROADCAST) {
             if ((info.addressType & DEST_BROADCAST) == 0) {
-                mSpamSafeLogger.warning("Unexpected broadcast message: " + message);
+                HdmiLogger.warning("Unexpected broadcast message: " + message);
                 return false;
             }
         } else {  // Direct addressing.
             if ((info.addressType & DEST_DIRECT) == 0) {
-                mSpamSafeLogger.warning("Unexpected direct message: " + message);
+                HdmiLogger.warning("Unexpected direct message: " + message);
                 return false;
             }
         }
 
         // Check the parameter type.
         if (!info.parameterValidator.isValid(message.getParams())) {
-            mSpamSafeLogger.warning("Unexpected parameters: " + message);
+            HdmiLogger.warning("Unexpected parameters: " + message);
             return false;
         }
         return true;
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 9314cf8..38c6fb3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -166,8 +166,6 @@
     // Type of logical devices hosted in the system. Stored in the unmodifiable list.
     private final List<Integer> mLocalDevices;
 
-    private final HdmiLogger mSpamSafeLogger = new HdmiLogger(TAG);
-
     // List of records for hotplug event listener to handle the the caller killed in action.
     @GuardedBy("mLock")
     private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
@@ -656,7 +654,7 @@
         if (mMessageValidator.isValid(command)) {
             mCecController.sendCommand(command, callback);
         } else {
-            mSpamSafeLogger.error("Invalid message type:" + command);
+            HdmiLogger.error("Invalid message type:" + command);
             if (callback != null) {
                 callback.onSendCompleted(Constants.SEND_RESULT_FAILURE);
             }
@@ -705,7 +703,7 @@
         }
 
         if (message.getDestination() != Constants.ADDR_BROADCAST) {
-            mSpamSafeLogger.warning("Unhandled cec command:" + message);
+            HdmiLogger.warning("Unhandled cec command:" + message);
         }
         return false;
     }
@@ -794,7 +792,6 @@
             // FLAG_HDMI_SYSTEM_AUDIO_VOLUME prevents audio manager from announcing
             // volume change notification back to hdmi control service.
             audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume,
-                    AudioManager.FLAG_SHOW_UI |
                     AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME);
         }
     }
@@ -2033,4 +2030,14 @@
         getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
                 HdmiControlService.PERMISSION);
     }
+
+    @ServiceThreadOnly
+    void displayOsd(int messageId, int extra) {
+        assertRunOnServiceThread();
+        Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE);
+        intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId);
+        intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_EXTRAM_PARAM1, extra);
+        getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
+                HdmiControlService.PERMISSION);
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/HdmiLogger.java b/services/core/java/com/android/server/hdmi/HdmiLogger.java
index c7add75..2562ffc 100644
--- a/services/core/java/com/android/server/hdmi/HdmiLogger.java
+++ b/services/core/java/com/android/server/hdmi/HdmiLogger.java
@@ -26,44 +26,83 @@
 /**
  * A logger that prevents spammy log. For the same log message, it logs once every 20seconds.
  * This class is not thread-safe.
+ * <p>
+ * For convenience, use single character prefix for all messages.
+ * Here are common acronyms
+ * <ul>
+ *   <li>[T]: Timout
+ *   <li>[R]: Received message
+ *   <li>[S]: Sent message
+ *   <li>[P]: Device polling result
+ * </ul>
  */
 final class HdmiLogger {
+    private static final String TAG = "HDMI";
     // Logging duration for same error message.
     private static final long ERROR_LOG_DURATTION_MILLIS = 20 * 1000;  // 20s
 
     private static final boolean DEBUG = false;
 
+    private static final ThreadLocal<HdmiLogger> sLogger = new ThreadLocal<>();
+
     // Key (String): log message.
     // Value (Pair(Long, Integer)): a pair of last log time millis and the number of logMessage.
     // Cache for warning.
     private final HashMap<String, Pair<Long, Integer>> mWarningTimingCache = new HashMap<>();
     // Cache for error.
     private final HashMap<String, Pair<Long, Integer>> mErrorTimingCache = new HashMap<>();
-    private final String mTag;
 
-    HdmiLogger(String tag) {
-        mTag = "HDMI:" + tag;
+    private HdmiLogger() {
     }
 
-    void warning(String logMessage) {
+    static final void warning(String logMessage, Object... objs) {
+        getLogger().warningInternal(toLogString(logMessage, objs));
+    }
+
+    private void warningInternal(String logMessage) {
         String log = updateLog(mWarningTimingCache, logMessage);
         if (!log.isEmpty()) {
-            Slog.w(mTag, log);
+            Slog.w(TAG, log);
         }
     }
 
-    void error(String logMessage) {
+    static final void error(String logMessage, Object... objs) {
+        getLogger().errorInternal(toLogString(logMessage, objs));
+    }
+
+    private void errorInternal(String logMessage) {
         String log = updateLog(mErrorTimingCache, logMessage);
         if (!log.isEmpty()) {
-            Slog.e(mTag, log);
+            Slog.e(TAG, log);
         }
     }
 
-    void debug(String logMessage) {
+    static final void debug(String logMessage, Object... objs) {
+        getLogger().debugInternal(toLogString(logMessage, objs));
+    }
+
+    private void debugInternal(String logMessage) {
         if (!DEBUG) {
             return;
         }
-        Slog.d(mTag, logMessage);
+        Slog.d(TAG, logMessage);
+    }
+
+    private static final String toLogString(String logMessage, Object[] objs) {
+        if (objs.length > 0) {
+            return String.format(logMessage, objs);
+        } else {
+            return logMessage;
+        }
+    }
+
+    private static HdmiLogger getLogger() {
+        HdmiLogger logger = sLogger.get();
+        if (logger == null) {
+            logger = new HdmiLogger();
+            sLogger.set(logger);
+        }
+        return logger;
     }
 
     private static String updateLog(HashMap<String, Pair<Long, Integer>> cache, String logMessage) {
diff --git a/services/core/java/com/android/server/hdmi/RequestArcAction.java b/services/core/java/com/android/server/hdmi/RequestArcAction.java
index 17c2d6c..3fb450f 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcAction.java
@@ -85,6 +85,7 @@
         if (mState != state || state != STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE) {
             return;
         }
+        HdmiLogger.debug("[T]RequestArcAction.");
         disableArcTransmission();
         finish();
     }
diff --git a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
index f25363d..d9e1f24 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
@@ -35,15 +35,15 @@
 
     @Override
     boolean start() {
+        mState = STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE;
+        addTimer(mState, HdmiConfig.TIMEOUT_MS);
+
         HdmiCecMessage command = HdmiCecMessageBuilder.buildRequestArcInitiation(
                 getSourceAddress(), mAvrAddress);
         sendCommand(command, new HdmiControlService.SendMessageCallback() {
             @Override
             public void onSendCompleted(int error) {
-                if (error == Constants.SEND_RESULT_SUCCESS) {
-                    mState = STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE;
-                    addTimer(mState, HdmiConfig.TIMEOUT_MS);
-                } else {
+                if (error != Constants.SEND_RESULT_SUCCESS) {
                     // If failed to send <Request ARC Initiation>, start "Disabled"
                     // ARC transmission action.
                     disableArcTransmission();
diff --git a/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java b/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
index 1491c72..f5a0115 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
@@ -35,15 +35,15 @@
 
     @Override
     boolean start() {
+        mState = STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE;
+        addTimer(mState, HdmiConfig.TIMEOUT_MS);
+
         HdmiCecMessage command =
                 HdmiCecMessageBuilder.buildRequestArcTermination(getSourceAddress(), mAvrAddress);
         sendCommand(command, new HdmiControlService.SendMessageCallback() {
             @Override
             public void onSendCompleted(int error) {
-                if (error == Constants.SEND_RESULT_SUCCESS) {
-                    mState = STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE;
-                    addTimer(mState, HdmiConfig.TIMEOUT_MS);
-                } else {
+                if (error != Constants.SEND_RESULT_SUCCESS) {
                     // If failed to send <Request ARC Termination>, start "Disabled" ARC
                     // transmission action.
                     disableArcTransmission();
diff --git a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
index 30519f3..bffa854 100644
--- a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
+++ b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
@@ -53,6 +53,18 @@
     @Override
     boolean start() {
         if (mEnabled) {
+            // Enable ARC status immediately after sending <Report Arc Initiated>.
+            // If AVR responds with <Feature Abort>, disable ARC status again.
+            // This is different from spec that says that turns ARC status to
+            // "Enabled" if <Report ARC Initiated> is acknowledged and no
+            // <Feature Abort> is received.
+            // But implemented this way to save the time having to wait for
+            // <Feature Abort>.
+            setArcStatus(true);
+            // If succeeds to send <Report ARC Initiated>, wait general timeout
+            // to check whether there is no <Feature Abort> for <Report ARC Initiated>.
+            mState = STATE_WAITING_TIMEOUT;
+            addTimer(mState, HdmiConfig.TIMEOUT_MS);
             sendReportArcInitiated();
         } else {
             setArcStatus(false);
@@ -67,23 +79,11 @@
         sendCommand(command, new HdmiControlService.SendMessageCallback() {
             @Override
             public void onSendCompleted(int error) {
-                if (error == Constants.SEND_RESULT_SUCCESS) {
-                    // Enable ARC status immediately after sending <Report Arc Initiated>.
-                    // If AVR responds with <Feature Abort>, disable ARC status again.
-                    // This is different from spec that says that turns ARC status to
-                    // "Enabled" if <Report ARC Initiated> is acknowledged and no
-                    // <Feature Abort> is received.
-                    // But implemented this way to save the time having to wait for
-                    // <Feature Abort>.
-                    setArcStatus(true);
-                    // If succeeds to send <Report ARC Initiated>, wait general timeout
-                    // to check whether there is no <Feature Abort> for <Report ARC Initiated>.
-                    mState = STATE_WAITING_TIMEOUT;
-                    addTimer(mState, HdmiConfig.TIMEOUT_MS);
-                } else {
+                if (error != Constants.SEND_RESULT_SUCCESS) {
                     // If fails to send <Report ARC Initiated>, disable ARC and
                     // send <Report ARC Terminated> directly.
                     setArcStatus(false);
+                    HdmiLogger.debug("Failed to send <Report Arc Initiated>.");
                     finish();
                 }
             }
@@ -112,6 +112,7 @@
         if (opcode == Constants.MESSAGE_FEATURE_ABORT) {
             int originalOpcode = cmd.getParams()[0] & 0xFF;
             if (originalOpcode == Constants.MESSAGE_REPORT_ARC_INITIATED) {
+                HdmiLogger.debug("Feature aborted for <Report Arc Initiated>");
                 setArcStatus(false);
                 finish();
                 return true;
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
index d15ffb0..0871194 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
@@ -98,7 +98,7 @@
             @Override
             public void onSendCompleted(int error) {
                 if (error != Constants.SEND_RESULT_SUCCESS) {
-                    DLOGGER.debug("Failed to send <System Audio Mode Request>:" + error);
+                    HdmiLogger.debug("Failed to send <System Audio Mode Request>:" + error);
                     setSystemAudioMode(false);
                     finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
                 }
@@ -111,7 +111,7 @@
     private void handleSendSystemAudioModeRequestTimeout() {
         if (!mTargetAudioStatus  // Don't retry for Off case.
                 || mSendRetryCount++ >= MAX_SEND_RETRY_COUNT) {
-            DLOGGER.debug("[T]:wait for <Set System Audio Mode>.");
+            HdmiLogger.debug("[T]:wait for <Set System Audio Mode>.");
             setSystemAudioMode(false);
             finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
             return;
@@ -130,7 +130,7 @@
                 if (cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT
                         && (cmd.getParams()[0] & 0xFF)
                                 == Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST) {
-                    DLOGGER.debug("Failed to start system audio mode request.");
+                    HdmiLogger.debug("Failed to start system audio mode request.");
                     setSystemAudioMode(false);
                     finishWithCallback(HdmiControlManager.RESULT_EXCEPTION);
                     return true;
@@ -145,7 +145,7 @@
                     startAudioStatusAction();
                     return true;
                 } else {
-                    DLOGGER.debug("Unexpected system audio mode request:" + receivedStatus);
+                    HdmiLogger.debug("Unexpected system audio mode request:" + receivedStatus);
                     // Unexpected response, consider the request is newly initiated by AVR.
                     // To return 'false' will initiate new SystemAudioActionFromAvr by the control
                     // service.
diff --git a/services/core/java/com/android/server/hdmi/VolumeControlAction.java b/services/core/java/com/android/server/hdmi/VolumeControlAction.java
index ddc267a..4338fc7 100644
--- a/services/core/java/com/android/server/hdmi/VolumeControlAction.java
+++ b/services/core/java/com/android/server/hdmi/VolumeControlAction.java
@@ -17,66 +17,35 @@
 package com.android.server.hdmi;
 
 import static com.android.server.hdmi.Constants.IRT_MS;
+import static com.android.server.hdmi.Constants.MESSAGE_FEATURE_ABORT;
+import static com.android.server.hdmi.Constants.MESSAGE_REPORT_AUDIO_STATUS;
+import static com.android.server.hdmi.Constants.MESSAGE_USER_CONTROL_PRESSED;
 
-import com.android.internal.util.Preconditions;
+import android.media.AudioManager;
 
 /**
  * Feature action that transmits volume change to Audio Receiver.
  * <p>
- * This action is created when a user pressed volume up/down. However, Since Android only provides a
- * listener for delta of some volume change, we will set a target volume, and check reported volume
- * from Audio Receiver(AVR). If TV receives no &lt;Report Audio Status&gt; from AVR, this action
- * will be finished in {@link #IRT_MS} * {@link #VOLUME_CHANGE_TIMEOUT_MAX_COUNT} (ms).
+ * This action is created when a user pressed volume up/down. However, Android only provides a
+ * listener for delta of some volume change instead of individual key event. Also it's hard to know
+ * Audio Receiver's number of volume steps for a single volume control key. Because of this, it
+ * sends key-down event until IRT timeout happens, and it will send key-up event if no additional
+ * volume change happens; otherwise, it will send again key-down as press and hold feature does.
  */
 final class VolumeControlAction extends HdmiCecFeatureAction {
     private static final String TAG = "VolumeControlAction";
 
-    private static final int VOLUME_MUTE = 101;
-    private static final int VOLUME_RESTORE = 102;
+    // State that wait for next volume press.
+    private static final int STATE_WAIT_FOR_NEXT_VOLUME_PRESS = 1;
     private static final int MAX_VOLUME = 100;
-    private static final int MIN_VOLUME = 0;
 
-    // State where to wait for <Report Audio Status>
-    private static final int STATE_WAIT_FOR_REPORT_VOLUME_STATUS = 1;
-
-    // Maximum count of time out used to finish volume action.
-    private static final int VOLUME_CHANGE_TIMEOUT_MAX_COUNT = 2;
+    private static final int UNKNOWN_AVR_VOLUME = -1;
 
     private final int mAvrAddress;
-    private final int mTargetVolume;
-    private final boolean mIsVolumeUp;
-    private int mTimeoutCount;
-
-    /**
-     * Create a {@link VolumeControlAction} for mute/restore change
-     *
-     * @param source source device sending volume change
-     * @param avrAddress address of audio receiver
-     * @param mute whether to mute sound or not. {@code true} for mute on; {@code false} for mute
-     *            off, i.e restore volume
-     * @return newly created {@link VolumeControlAction}
-     */
-    public static VolumeControlAction ofMute(HdmiCecLocalDevice source, int avrAddress,
-            boolean mute) {
-        return new VolumeControlAction(source, avrAddress, mute ? VOLUME_MUTE : VOLUME_RESTORE,
-                false);
-    }
-
-    /**
-     * Create a {@link VolumeControlAction} for volume up/down change
-     *
-     * @param source source device sending volume change
-     * @param avrAddress address of audio receiver
-     * @param targetVolume target volume to be set to AVR. It should be in range of [0-100]
-     * @param isVolumeUp whether to volume up or not. {@code true} for volume up; {@code false} for
-     *            volume down
-     * @return newly created {@link VolumeControlAction}
-     */
-    public static VolumeControlAction ofVolumeChange(HdmiCecLocalDevice source, int avrAddress,
-            int targetVolume, boolean isVolumeUp) {
-        Preconditions.checkArgumentInRange(targetVolume, MIN_VOLUME, MAX_VOLUME, "volume");
-        return new VolumeControlAction(source, avrAddress, targetVolume, isVolumeUp);
-    }
+    private boolean mIsVolumeUp;
+    private long mLastKeyUpdateTime;
+    private int mLastAvrVolume;
+    private boolean mSentKeyPressed;
 
     /**
      * Scale a custom volume value to cec volume scale.
@@ -94,123 +63,141 @@
      *
      * @param cecVolume volume value in cec volume scale. It should be in a range of [0-100]
      * @param scale scale of custom volume (max volume)
-     * @return a volume value scaled to custom volume range
+     * @return a volume scaled to custom volume range
      */
     public static int scaleToCustomVolume(int cecVolume, int scale) {
         return (cecVolume * scale) / MAX_VOLUME;
     }
 
-    private VolumeControlAction(HdmiCecLocalDevice source, int avrAddress, int targetVolume,
-            boolean isVolumeUp) {
+    VolumeControlAction(HdmiCecLocalDevice source, int avrAddress, boolean isVolumeUp) {
         super(source);
-
         mAvrAddress = avrAddress;
-        mTargetVolume = targetVolume;
         mIsVolumeUp = isVolumeUp;
+        mLastAvrVolume = UNKNOWN_AVR_VOLUME;
+        mSentKeyPressed = false;
+
+        updateLastKeyUpdateTime();
+    }
+
+    private void updateLastKeyUpdateTime() {
+        mLastKeyUpdateTime = System.currentTimeMillis();
     }
 
     @Override
     boolean start() {
-        if (isForMute()) {
-            sendMuteChange(mTargetVolume == VOLUME_MUTE);
-            finish();
-            return true;
-        }
-
-        startVolumeChange();
+        mState = STATE_WAIT_FOR_NEXT_VOLUME_PRESS;
+        sendVolumeKeyPressed();
+        resetTimer();
         return true;
     }
 
-
-    private boolean isForMute() {
-        return mTargetVolume == VOLUME_MUTE || mTargetVolume == VOLUME_RESTORE;
-    }
-
-    private void startVolumeChange() {
-        mTimeoutCount = 0;
-        sendVolumeChange(mIsVolumeUp);
-        mState = STATE_WAIT_FOR_REPORT_VOLUME_STATUS;
-        addTimer(mState, IRT_MS);
-    }
-
-    private void sendVolumeChange(boolean up) {
+    private void sendVolumeKeyPressed() {
         sendCommand(HdmiCecMessageBuilder.buildUserControlPressed(getSourceAddress(), mAvrAddress,
-                up ? HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP
+                mIsVolumeUp ? HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP
                         : HdmiCecKeycode.CEC_KEYCODE_VOLUME_DOWN));
+        mSentKeyPressed = true;
     }
 
-    private void sendMuteChange(boolean mute) {
-        sendUserControlPressedAndReleased(mAvrAddress,
-                mute ? HdmiCecKeycode.CEC_KEYCODE_MUTE_FUNCTION :
-                        HdmiCecKeycode.CEC_KEYCODE_RESTORE_VOLUME_FUNCTION);
+    private void resetTimer() {
+        mActionTimer.clearTimerMessage();
+        addTimer(STATE_WAIT_FOR_NEXT_VOLUME_PRESS, IRT_MS);
+    }
+
+    void handleVolumeChange(boolean isVolumeUp) {
+        if (mIsVolumeUp != isVolumeUp) {
+            HdmiLogger.debug("Volume Key Status Changed[old:%b new:%b]", mIsVolumeUp, isVolumeUp);
+            sendVolumeKeyReleased();
+            mIsVolumeUp = isVolumeUp;
+        }
+        updateLastKeyUpdateTime();
+    }
+
+    private void sendVolumeKeyReleased() {
+        sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(
+                getSourceAddress(), mAvrAddress));
+        mSentKeyPressed = false;
     }
 
     @Override
     boolean processCommand(HdmiCecMessage cmd) {
-        if (mState != STATE_WAIT_FOR_REPORT_VOLUME_STATUS) {
+        if (mState != STATE_WAIT_FOR_NEXT_VOLUME_PRESS || cmd.getSource() != mAvrAddress) {
             return false;
         }
 
         switch (cmd.getOpcode()) {
-            case Constants.MESSAGE_REPORT_AUDIO_STATUS:
-                handleReportAudioStatus(cmd);
-                return true;
-            case Constants.MESSAGE_FEATURE_ABORT:
-                int originalOpcode = cmd.getParams()[0] & 0xFF;
-                if (originalOpcode == Constants.MESSAGE_USER_CONTROL_PRESSED
-                        || originalOpcode == Constants.MESSAGE_USER_CONTROL_RELEASED) {
-                    // TODO: handle feature abort.
-                    finish();
-                    return true;
-                }
-            default:  // fall through
+            case MESSAGE_REPORT_AUDIO_STATUS:
+                return handleReportAudioStatus(cmd);
+            case MESSAGE_FEATURE_ABORT:
+                return handleFeatureAbort(cmd);
+            default:
                 return false;
         }
     }
 
-    private void handleReportAudioStatus(HdmiCecMessage cmd) {
-        byte[] params = cmd.getParams();
+    private boolean handleReportAudioStatus(HdmiCecMessage cmd) {
+        byte params[] = cmd.getParams();
+        boolean mute = (params[0] & 0x80) == 0x80;
         int volume = params[0] & 0x7F;
-        // Update volume with new value.
-        // Note that it will affect system volume change.
-        tv().setAudioStatus(false, volume);
-        if (mIsVolumeUp) {
-            if (mTargetVolume <= volume) {
-                finishWithVolumeChangeRelease();
-                return;
-            }
-        } else {
-            if (mTargetVolume >= volume) {
-                finishWithVolumeChangeRelease();
-                return;
-            }
+        mLastAvrVolume = volume;
+        if (shouldUpdateAudioVolume(mute)) {
+            HdmiLogger.debug("Force volume change[mute:%b, volume=%d]", mute, volume);
+            tv().setAudioStatus(mute, volume);
         }
-
-        // Clear action status and send another volume change command.
-        clear();
-        startVolumeChange();
+        return true;
     }
 
-    private void finishWithVolumeChangeRelease() {
-        sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(
-                getSourceAddress(), mAvrAddress));
-        finish();
+    private boolean shouldUpdateAudioVolume(boolean mute) {
+        // Do nothing if in mute.
+        if (mute) {
+            return true;
+        }
+
+        // Update audio status if current volume position is edge of volume bar,
+        // i.e max or min volume.
+        AudioManager audioManager = tv().getService().getAudioManager();
+        int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+        if (mIsVolumeUp) {
+            int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+            return currentVolume == maxVolume;
+        } else {
+            return currentVolume == 0;
+        }
+    }
+
+    private boolean handleFeatureAbort(HdmiCecMessage cmd) {
+        int originalOpcode = cmd.getParams()[0] & 0xFF;
+        // Since it sends <User Control Released> only when it finishes this action,
+        // it takes care of <User Control Pressed> only here.
+        if (originalOpcode == MESSAGE_USER_CONTROL_PRESSED) {
+            finish();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void clear() {
+        super.clear();
+        if (mSentKeyPressed) {
+            sendVolumeKeyReleased();
+        }
+        if (mLastAvrVolume != UNKNOWN_AVR_VOLUME) {
+            tv().setAudioStatus(false, mLastAvrVolume);
+            mLastAvrVolume = UNKNOWN_AVR_VOLUME;
+        }
     }
 
     @Override
     void handleTimerEvent(int state) {
-        if (mState != STATE_WAIT_FOR_REPORT_VOLUME_STATUS) {
+        if (state != STATE_WAIT_FOR_NEXT_VOLUME_PRESS) {
             return;
         }
 
-        // If no report volume action after IRT * VOLUME_CHANGE_TIMEOUT_MAX_COUNT just stop volume
-        // action.
-        if (++mTimeoutCount == VOLUME_CHANGE_TIMEOUT_MAX_COUNT) {
-            finishWithVolumeChangeRelease();
-            return;
+        if (System.currentTimeMillis() - mLastKeyUpdateTime >= IRT_MS) {
+            finish();
+        } else {
+            sendVolumeKeyPressed();
+            resetTimer();
         }
-
-        sendVolumeChange(mIsVolumeUp);
-        addTimer(mState, IRT_MS);
     }
 }