Add for System Audio Mode

Note that this is skeleton change and do not merge
till get full review from outside of CEC team.

This change introduce four apis for System Audio Mode

1. boolean canChangeSystemAudioMode()
  -  Whether to change system audio mode or not.

2. setSystemAudioMode(boolean enabled, IHdmiControlCallback callback);
  - Change system audio mode.

3. add/removeSystemAudioModeChangeLister.
  - Register/deregister listner for AudioModeChange.

4. getSystemAudioMode()
  - Whether to system audio is enabled or not.

Change-Id: I1e82365155a9f7f6c3ac5d9db4871cf6bad46865
diff --git a/Android.mk b/Android.mk
index 8f7779e..936b41a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -152,6 +152,7 @@
 	core/java/android/hardware/hdmi/IHdmiControlService.aidl \
 	core/java/android/hardware/hdmi/IHdmiDeviceEventListener.aidl \
 	core/java/android/hardware/hdmi/IHdmiHotplugEventListener.aidl \
+	core/java/android/hardware/hdmi/IHdmiSystemAudioModeChangeListener.aidl \
 	core/java/android/hardware/input/IInputManager.aidl \
 	core/java/android/hardware/input/IInputDevicesChangedListener.aidl \
 	core/java/android/hardware/location/IFusedLocationHardware.aidl \
diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
index 559a469..73726fa 100644
--- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl
+++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
@@ -21,6 +21,7 @@
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.hdmi.IHdmiDeviceEventListener;
 import android.hardware.hdmi.IHdmiHotplugEventListener;
+import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
 
 import java.util.List;
 
@@ -42,4 +43,9 @@
     void portSelect(int portId, IHdmiControlCallback callback);
     void sendKeyEvent(int keyCode, boolean isPressed);
     List<HdmiPortInfo> getPortInfo();
+    boolean canChangeSystemAudioMode();
+    boolean getSystemAudioMode();
+    void setSystemAudioMode(boolean enabled, IHdmiControlCallback callback);
+    void addSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener);
+    void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener);
 }
diff --git a/core/java/android/hardware/hdmi/IHdmiSystemAudioModeChangeListener.aidl b/core/java/android/hardware/hdmi/IHdmiSystemAudioModeChangeListener.aidl
new file mode 100644
index 0000000..714bbe7
--- /dev/null
+++ b/core/java/android/hardware/hdmi/IHdmiSystemAudioModeChangeListener.aidl
@@ -0,0 +1,31 @@
+/*
+ * 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 android.hardware.hdmi;
+
+/**
+ * Callback interface definition for HDMI client to get informed of
+ * "System Audio" mode change.
+ *
+ * @hide
+ */
+oneway interface IHdmiSystemAudioModeChangeListener {
+
+    /**
+     * @param enabled true if the device gets activated
+     */
+    void onStatusChanged(in boolean enabled);
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 52c092c..1210e10 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -253,20 +253,35 @@
         mDeviceInfos.clear();
     }
 
+    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
+        assertRunOnServiceThread();
+        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
+        if (avr == null) {
+            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
+            return;
+        }
+
+        addAndStartAction(
+                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
+   }
+
+    boolean canChangeSystemAudioMode() {
+        // TODO: once have immutable device info, test whether avr info exists or not.
+        return false;
+    }
+
     void setSystemAudioMode(boolean on) {
         synchronized (mLock) {
             if (on != mSystemAudioMode) {
                 mSystemAudioMode = on;
                 // TODO: Need to set the preference for SystemAudioMode.
-                // TODO: Need to handle the notification of changing the mode and
-                // to identify the notification should be handled in the service or TvSettings.
+                mService.announceSystemAudioModeChange(on);
             }
         }
     }
 
     boolean getSystemAudioMode() {
         synchronized (mLock) {
-            assertRunOnServiceThread();
             return mSystemAudioMode;
         }
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index d323f34..7774878 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -27,6 +27,7 @@
 import android.hardware.hdmi.IHdmiControlService;
 import android.hardware.hdmi.IHdmiDeviceEventListener;
 import android.hardware.hdmi.IHdmiHotplugEventListener;
+import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -113,6 +114,14 @@
     private final ArrayList<DeviceEventListenerRecord> mDeviceEventListenerRecords =
             new ArrayList<>();
 
+    // List of listeners registered by callers that want to get notified of
+    // system audio mode changes.
+    private final ArrayList<IHdmiSystemAudioModeChangeListener>
+            mSystemAudioModeChangeListeners = new ArrayList<>();
+    // List of records for system audio mode change to handle the the caller killed in action.
+    private final ArrayList<SystemAudioModeChangeListenerRecord>
+            mSystemAudioModeChangeListenerRecords = new ArrayList<>();
+
     // Handler used to run a task in service thread.
     private final Handler mHandler = new Handler();
 
@@ -447,6 +456,12 @@
         // TODO: Hook up with AudioManager.
     }
 
+    void announceSystemAudioModeChange(boolean enabled) {
+        for (IHdmiSystemAudioModeChangeListener listener : mSystemAudioModeChangeListeners) {
+            invokeSystemAudioModeChange(listener, enabled);
+        }
+    }
+
     private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
         // TODO: find better name instead of model name.
         String displayName = Build.MODEL;
@@ -480,7 +495,7 @@
         }
 
         @Override
-            public void binderDied() {
+        public void binderDied() {
             synchronized (mLock) {
                 mDeviceEventListenerRecords.remove(this);
                 mDeviceEventListeners.remove(mListener);
@@ -488,6 +503,22 @@
         }
     }
 
+    private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient {
+        private IHdmiSystemAudioModeChangeListener mListener;
+
+        public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) {
+            mListener = listener;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mLock) {
+                mSystemAudioModeChangeListenerRecords.remove(this);
+                mSystemAudioModeChangeListeners.remove(mListener);
+            }
+        }
+    }
+
     private void enforceAccessPermission() {
         getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
     }
@@ -605,7 +636,7 @@
             enforceAccessPermission();
             runOnServiceThread(new Runnable() {
                 @Override
-                    public void run() {
+                public void run() {
                     HdmiControlService.this.addDeviceEventListener(listener);
                 }
             });
@@ -616,6 +647,57 @@
             enforceAccessPermission();
             return mPortInfo;
         }
+
+        @Override
+        public boolean canChangeSystemAudioMode() {
+            enforceAccessPermission();
+            HdmiCecLocalDeviceTv tv = tv();
+            if (tv == null) {
+                return false;
+            }
+            return tv.canChangeSystemAudioMode();
+        }
+
+        @Override
+        public boolean getSystemAudioMode() {
+            enforceAccessPermission();
+            HdmiCecLocalDeviceTv tv = tv();
+            if (tv == null) {
+                return false;
+            }
+            return tv.getSystemAudioMode();
+        }
+
+        @Override
+        public void setSystemAudioMode(final boolean enabled, final IHdmiControlCallback callback) {
+            enforceAccessPermission();
+            runOnServiceThread(new Runnable() {
+                @Override
+                public void run() {
+                    HdmiCecLocalDeviceTv tv = tv();
+                    if (tv == null) {
+                        Slog.w(TAG, "Local tv device not available");
+                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
+                        return;
+                    }
+                    tv.changeSystemAudioMode(enabled, callback);
+                }
+            });
+        }
+
+        @Override
+        public void addSystemAudioModeChangeListener(
+                final IHdmiSystemAudioModeChangeListener listener) {
+            enforceAccessPermission();
+            HdmiControlService.this.addSystemAudioModeChangeListner(listener);
+        }
+
+        @Override
+        public void removeSystemAudioModeChangeListener(
+                final IHdmiSystemAudioModeChangeListener listener) {
+            enforceAccessPermission();
+            HdmiControlService.this.removeSystemAudioModeChangeListener(listener);
+        }
     }
 
     private void oneTouchPlay(final IHdmiControlCallback callback) {
@@ -693,6 +775,35 @@
         }
     }
 
+    private void addSystemAudioModeChangeListner(IHdmiSystemAudioModeChangeListener listener) {
+        SystemAudioModeChangeListenerRecord record = new SystemAudioModeChangeListenerRecord(
+                listener);
+        try {
+            listener.asBinder().linkToDeath(record, 0);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Listener already died");
+            return;
+        }
+        synchronized (mLock) {
+            mSystemAudioModeChangeListeners.add(listener);
+            mSystemAudioModeChangeListenerRecords.add(record);
+        }
+    }
+
+    private void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) {
+        synchronized (mLock) {
+            for (SystemAudioModeChangeListenerRecord record :
+                    mSystemAudioModeChangeListenerRecords) {
+                if (record.mListener.asBinder() == listener) {
+                    listener.asBinder().unlinkToDeath(record, 0);
+                    mSystemAudioModeChangeListenerRecords.remove(record);
+                    break;
+                }
+            }
+            mSystemAudioModeChangeListeners.remove(listener);
+        }
+    }
+
     private void invokeCallback(IHdmiControlCallback callback, int result) {
         try {
             callback.onComplete(result);
@@ -701,6 +812,15 @@
         }
     }
 
+    private void invokeSystemAudioModeChange(IHdmiSystemAudioModeChangeListener listener,
+            boolean enabled) {
+        try {
+            listener.onStatusChanged(enabled);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Invoking callback failed:" + e);
+        }
+    }
+
     private void announceHotplugEvent(int portId, boolean connected) {
         HdmiHotplugEvent event = new HdmiHotplugEvent(portId, connected);
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
index 92418ab..959a38e 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
@@ -16,8 +16,12 @@
 
 package com.android.server.hdmi;
 
+import android.annotation.Nullable;
 import android.hardware.hdmi.HdmiCec;
 import android.hardware.hdmi.HdmiCecMessage;
+import android.hardware.hdmi.IHdmiControlCallback;
+import android.os.RemoteException;
+import android.util.Slog;
 
 /**
  * Base feature action class for SystemAudioActionFromTv and SystemAudioActionFromAvr.
@@ -39,6 +43,8 @@
     // The target audio status of the action, whether to enable the system audio mode or not.
     protected boolean mTargetAudioStatus;
 
+    @Nullable private final IHdmiControlCallback mCallback;
+
     private int mSendRetryCount = 0;
 
     /**
@@ -47,13 +53,16 @@
      * @param source {@link HdmiCecLocalDevice} instance
      * @param avrAddress logical address of AVR device
      * @param targetStatus Whether to enable the system audio mode or not
+     * @param callback callback interface to be notified when it's done
      * @throw IllegalArugmentException if device type of sourceAddress and avrAddress is invalid
      */
-    SystemAudioAction(HdmiCecLocalDevice source, int avrAddress, boolean targetStatus) {
+    SystemAudioAction(HdmiCecLocalDevice source, int avrAddress, boolean targetStatus,
+            IHdmiControlCallback callback) {
         super(source);
         HdmiUtils.verifyAddressType(avrAddress, HdmiCec.DEVICE_AUDIO_SYSTEM);
         mAvrLogicalAddress = avrAddress;
         mTargetAudioStatus = targetStatus;
+        mCallback = callback;
     }
 
     protected void sendSystemAudioModeRequest() {
@@ -69,7 +78,7 @@
                     addTimer(mState, mTargetAudioStatus ? ON_TIMEOUT_MS : OFF_TIMEOUT_MS);
                 } else {
                     setSystemAudioMode(false);
-                    finish();
+                    finishWithCallback(HdmiCec.RESULT_EXCEPTION);
                 }
             }
         });
@@ -79,7 +88,7 @@
         if (!mTargetAudioStatus  // Don't retry for Off case.
                 || mSendRetryCount++ >= MAX_SEND_RETRY_COUNT) {
             setSystemAudioMode(false);
-            finish();
+            finishWithCallback(HdmiCec.RESULT_TIMEOUT);
             return;
         }
         sendSystemAudioModeRequest();
@@ -107,7 +116,7 @@
                     // Unexpected response, consider the request is newly initiated by AVR.
                     // To return 'false' will initiate new SystemAudioActionFromAvr by the control
                     // service.
-                    finish();
+                    finishWithCallback(HdmiCec.RESULT_EXCEPTION);
                     return false;
                 }
             default:
@@ -116,7 +125,7 @@
     }
 
     protected void startAudioStatusAction() {
-        addAndStartAction(new SystemAudioStatusAction(tv(), mAvrLogicalAddress));
+        addAndStartAction(new SystemAudioStatusAction(tv(), mAvrLogicalAddress, mCallback));
         finish();
     }
 
@@ -136,4 +145,17 @@
                 return;
         }
     }
+
+    // TODO: if IHdmiControlCallback is general to other FeatureAction,
+    //       move it into FeatureAction.
+    protected void finishWithCallback(int returnCode) {
+        if (mCallback != null) {
+            try {
+                mCallback.onComplete(returnCode);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to invoke callback.", e);
+            }
+        }
+        finish();
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
index b743c64..9d34589 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
@@ -17,6 +17,7 @@
 package com.android.server.hdmi;
 
 import android.hardware.hdmi.HdmiCec;
+import android.hardware.hdmi.IHdmiControlCallback;
 
 /**
  * Feature action that handles System Audio initiated by AVR devices.
@@ -28,11 +29,12 @@
      * @param source {@link HdmiCecLocalDevice} instance
      * @param avrAddress logical address of AVR device
      * @param targetStatus Whether to enable the system audio mode or not
+     * @param callback callback interface to be notified when it's done
      * @throw IllegalArugmentException if device type of tvAddress and avrAddress is invalid
      */
     SystemAudioActionFromAvr(HdmiCecLocalDevice source, int avrAddress,
-            boolean targetStatus) {
-        super(source, avrAddress, targetStatus);
+            boolean targetStatus, IHdmiControlCallback callback) {
+        super(source, avrAddress, targetStatus, callback);
         HdmiUtils.verifyAddressType(getSourceAddress(), HdmiCec.DEVICE_TV);
     }
 
@@ -45,7 +47,7 @@
 
     private void handleSystemAudioActionFromAvr() {
         if (mTargetAudioStatus == tv().getSystemAudioMode()) {
-            finish();
+            finishWithCallback(HdmiCec.RESULT_SUCCESS);
             return;
         }
         if (tv().isInPresetInstallationMode()) {
@@ -62,7 +64,7 @@
             startAudioStatusAction();
         } else {
             setSystemAudioMode(false);
-            finish();
+            finishWithCallback(HdmiCec.RESULT_SUCCESS);
         }
     }
 }
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java b/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java
index e0c4ff4..2d8f3fc 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioActionFromTv.java
@@ -17,6 +17,7 @@
 package com.android.server.hdmi;
 
 import android.hardware.hdmi.HdmiCec;
+import android.hardware.hdmi.IHdmiControlCallback;
 
 
 /**
@@ -29,11 +30,12 @@
      * @param sourceAddress {@link HdmiCecLocalDevice} instance
      * @param avrAddress logical address of AVR device
      * @param targetStatus Whether to enable the system audio mode or not
+     * @param callback callback interface to be notified when it's done
      * @throw IllegalArugmentException if device type of tvAddress is invalid
      */
     SystemAudioActionFromTv(HdmiCecLocalDevice sourceAddress, int avrAddress,
-            boolean targetStatus) {
-        super(sourceAddress, avrAddress, targetStatus);
+            boolean targetStatus, IHdmiControlCallback callback) {
+        super(sourceAddress, avrAddress, targetStatus, callback);
         HdmiUtils.verifyAddressType(getSourceAddress(), HdmiCec.DEVICE_TV);
     }
 
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
index e4d82ef..c7ab406 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
@@ -77,14 +77,14 @@
         // If the last setting is system audio, turn on system audio whatever AVR status is.
         if (tv().getSystemAudioMode()) {
             if (canChangeSystemAudio()) {
-                addAndStartAction(new SystemAudioActionFromTv(tv(), mAvrAddress, true));
+                addAndStartAction(new SystemAudioActionFromTv(tv(), mAvrAddress, true, null));
             }
         } else {
             // If the last setting is non-system audio, turn off system audio mode
             // and update system audio status (volume or mute).
             tv().setSystemAudioMode(false);
             if (canChangeSystemAudio()) {
-                addAndStartAction(new SystemAudioStatusAction(tv(), mAvrAddress));
+                addAndStartAction(new SystemAudioStatusAction(tv(), mAvrAddress, null));
             }
         }
         finish();
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
index 5f4fc23..89206a7 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
@@ -16,8 +16,11 @@
 
 package com.android.server.hdmi;
 
+import android.annotation.Nullable;
 import android.hardware.hdmi.HdmiCec;
 import android.hardware.hdmi.HdmiCecMessage;
+import android.hardware.hdmi.IHdmiControlCallback;
+import android.os.RemoteException;
 import android.util.Slog;
 
 import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
@@ -32,10 +35,13 @@
     private static final int STATE_WAIT_FOR_REPORT_AUDIO_STATUS = 1;
 
     private final int mAvrAddress;
+    @Nullable private final IHdmiControlCallback mCallback;
 
-    SystemAudioStatusAction(HdmiCecLocalDevice source, int avrAddress) {
+    SystemAudioStatusAction(HdmiCecLocalDevice source, int avrAddress,
+            IHdmiControlCallback callback) {
         super(source);
         mAvrAddress = avrAddress;
+        mCallback = callback;
     }
 
     @Override
@@ -67,7 +73,9 @@
                 ? HdmiConstants.UI_COMMAND_RESTORE_VOLUME_FUNCTION  // SystemAudioMode: ON
                 : HdmiConstants.UI_COMMAND_MUTE_FUNCTION;           // SystemAudioMode: OFF
         sendUserControlPressedAndReleased(uiCommand);
-        finish();
+
+        // Still return SUCCESS to callback.
+        finishWithCallback(HdmiCec.RESULT_SUCCESS);
     }
 
     private void sendUserControlPressedAndReleased(int uiCommand) {
@@ -103,7 +111,7 @@
                 // Toggle AVR's mute status to match with the system audio status.
                 sendUserControlPressedAndReleased(HdmiConstants.UI_COMMAND_MUTE);
             }
-            finish();
+            finishWithCallback(HdmiCec.RESULT_SUCCESS);
         } else {
             Slog.e(TAG, "Invalid <Report Audio Status> message:" + cmd);
             handleSendGiveAudioStatusFailure();
@@ -111,6 +119,17 @@
         }
     }
 
+    private void finishWithCallback(int returnCode) {
+        if (mCallback != null) {
+            try {
+                mCallback.onComplete(returnCode);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to invoke callback.", e);
+            }
+        }
+        finish();
+    }
+
     @Override
     void handleTimerEvent(int state) {
         if (mState != state) {