[CEC Configuration] Add listener system API

Bug: 174731501
Bug: 172637440
Bug: 172905515
Test: atest com.google.android.gts.hdmicec.HdmiControlManagerHostTest
Change-Id: I830f3e312d5cf70fc84e389a12ecd50c2e1bfae6
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index a6dfdac..9386d23 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2741,6 +2741,8 @@
   }
 
   public final class HdmiControlManager {
+    method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void addHdmiCecEnabledChangeListener(@NonNull android.hardware.hdmi.HdmiControlManager.CecSettingChangeListener);
+    method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void addHdmiCecEnabledChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.hdmi.HdmiControlManager.CecSettingChangeListener);
     method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void addHotplugEventListener(android.hardware.hdmi.HdmiControlManager.HotplugEventListener);
     method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void addHotplugEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.hdmi.HdmiControlManager.HotplugEventListener);
     method @NonNull @RequiresPermission(android.Manifest.permission.HDMI_CEC) public java.util.List<java.lang.Integer> getAllowedCecSettingIntValues(@NonNull String);
@@ -2759,6 +2761,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.HDMI_CEC) public java.util.List<java.lang.String> getUserCecSettings();
     method public boolean isDeviceConnected(@NonNull android.hardware.hdmi.HdmiDeviceInfo);
     method public void powerOffDevice(@NonNull android.hardware.hdmi.HdmiDeviceInfo);
+    method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void removeHdmiCecEnabledChangeListener(@NonNull android.hardware.hdmi.HdmiControlManager.CecSettingChangeListener);
     method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void removeHotplugEventListener(android.hardware.hdmi.HdmiControlManager.HotplugEventListener);
     method public void setActiveSource(@NonNull android.hardware.hdmi.HdmiDeviceInfo);
     method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setHdmiCecEnabled(@NonNull int);
@@ -2871,6 +2874,10 @@
     field public static final int TIMER_STATUS_PROGRAMMED_INFO_NO_MEDIA_INFO = 10; // 0xa
   }
 
+  public static interface HdmiControlManager.CecSettingChangeListener {
+    method public void onChange(@NonNull String);
+  }
+
   @IntDef({android.hardware.hdmi.HdmiControlManager.RESULT_SUCCESS, android.hardware.hdmi.HdmiControlManager.RESULT_TIMEOUT, android.hardware.hdmi.HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE, android.hardware.hdmi.HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE, android.hardware.hdmi.HdmiControlManager.RESULT_ALREADY_IN_PROGRESS, android.hardware.hdmi.HdmiControlManager.RESULT_EXCEPTION, android.hardware.hdmi.HdmiControlManager.RESULT_INCORRECT_MODE, android.hardware.hdmi.HdmiControlManager.RESULT_COMMUNICATION_FAILED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public static @interface HdmiControlManager.ControlCallbackResult {
   }
 
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index 38ffc80..b09eda4 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -1372,6 +1372,87 @@
     }
 
     /**
+     * Listener used to get setting change notification.
+     *
+     * @hide
+     */
+    @SystemApi
+    public interface CecSettingChangeListener {
+        /**
+         * Called when value of a setting changes.
+         *
+         * @param setting name of a CEC setting that changed
+         */
+        void onChange(@NonNull @CecSettingName String setting);
+    }
+
+    private final ArrayMap<String,
+            ArrayMap<CecSettingChangeListener, IHdmiCecSettingChangeListener>>
+                    mCecSettingChangeListeners = new ArrayMap<>();
+
+    private void addCecSettingChangeListener(
+            @NonNull @CecSettingName String setting,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull CecSettingChangeListener listener) {
+        if (mService == null) {
+            Log.e(TAG, "HdmiControlService is not available");
+            return;
+        }
+        if (mCecSettingChangeListeners.containsKey(setting)
+                && mCecSettingChangeListeners.get(setting).containsKey(listener)) {
+            Log.e(TAG, "listener is already registered");
+            return;
+        }
+        IHdmiCecSettingChangeListener wrappedListener =
+                getCecSettingChangeListenerWrapper(executor, listener);
+        if (!mCecSettingChangeListeners.containsKey(setting)) {
+            mCecSettingChangeListeners.put(setting, new ArrayMap<>());
+        }
+        mCecSettingChangeListeners.get(setting).put(listener, wrappedListener);
+        try {
+            mService.addCecSettingChangeListener(setting, wrappedListener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private void removeCecSettingChangeListener(
+            @NonNull @CecSettingName String setting,
+            @NonNull CecSettingChangeListener listener) {
+        if (mService == null) {
+            Log.e(TAG, "HdmiControlService is not available");
+            return;
+        }
+        IHdmiCecSettingChangeListener wrappedListener =
+                !mCecSettingChangeListeners.containsKey(setting) ? null :
+                    mCecSettingChangeListeners.get(setting).remove(listener);
+        if (wrappedListener == null) {
+            Log.e(TAG, "tried to remove not-registered listener");
+            return;
+        }
+        try {
+            mService.removeCecSettingChangeListener(setting, wrappedListener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private IHdmiCecSettingChangeListener getCecSettingChangeListenerWrapper(
+            Executor executor, final CecSettingChangeListener listener) {
+        return new IHdmiCecSettingChangeListener.Stub() {
+            @Override
+            public void onChange(String setting) {
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    executor.execute(() -> listener.onChange(setting));
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            }
+        };
+    }
+
+    /**
      * Get a set of user-modifiable settings.
      *
      * @return a set of user-modifiable settings.
@@ -1493,6 +1574,53 @@
     }
 
     /**
+     * Add change listener for global status of HDMI CEC.
+     *
+     * <p>To stop getting the notification,
+     * use {@link #removeHdmiCecEnabledChangeListener(CecSettingChangeListener)}.
+     *
+     * Note that each invocation of the callback will be executed on an arbitrary
+     * Binder thread. This means that all callback implementations must be
+     * thread safe. To specify the execution thread, use
+     * {@link addHdmiCecEnabledChangeListener(Executor, CecSettingChangeListener)}.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.HDMI_CEC)
+    public void addHdmiCecEnabledChangeListener(@NonNull CecSettingChangeListener listener) {
+        addHdmiCecEnabledChangeListener(ConcurrentUtils.DIRECT_EXECUTOR, listener);
+    }
+
+    /**
+     * Add change listener for global status of HDMI CEC.
+     *
+     * <p>To stop getting the notification,
+     * use {@link #removeHdmiCecEnabledChangeListener(CecSettingChangeListener)}.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.HDMI_CEC)
+    public void addHdmiCecEnabledChangeListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull CecSettingChangeListener listener) {
+        addCecSettingChangeListener(CEC_SETTING_NAME_HDMI_CEC_ENABLED, executor, listener);
+    }
+
+    /**
+     * Remove change listener for global status of HDMI CEC.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.HDMI_CEC)
+    public void removeHdmiCecEnabledChangeListener(
+            @NonNull CecSettingChangeListener listener) {
+        removeCecSettingChangeListener(CEC_SETTING_NAME_HDMI_CEC_ENABLED, listener);
+    }
+
+    /**
      * Set the version of the HDMI CEC specification currently used.
      *
      * <p>Allows to select either CEC 1.4b or 2.0 to be used by the device.
diff --git a/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java
index 3b61911f..89a7afa8 100644
--- a/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java
+++ b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java
@@ -306,6 +306,18 @@
         }
 
         @Override
+        public void addCecSettingChangeListener(String name,
+                IHdmiCecSettingChangeListener listener) {
+            HdmiControlServiceWrapper.this.addCecSettingChangeListener(name, listener);
+        }
+
+        @Override
+        public void removeCecSettingChangeListener(String name,
+                IHdmiCecSettingChangeListener listener) {
+            HdmiControlServiceWrapper.this.removeCecSettingChangeListener(name, listener);
+        }
+
+        @Override
         public List<String> getUserCecSettings() {
             return HdmiControlServiceWrapper.this.getUserCecSettings();
         }
@@ -522,6 +534,14 @@
             IHdmiCecVolumeControlFeatureListener listener) {}
 
     /** @hide */
+    public void addCecSettingChangeListener(String name,
+            IHdmiCecSettingChangeListener listener) {}
+
+    /** @hide */
+    public void removeCecSettingChangeListener(String name,
+            IHdmiCecSettingChangeListener listener) {}
+
+    /** @hide */
     public List<String> getUserCecSettings() {
         return new ArrayList<>();
     }
diff --git a/core/java/android/hardware/hdmi/IHdmiCecSettingChangeListener.aidl b/core/java/android/hardware/hdmi/IHdmiCecSettingChangeListener.aidl
new file mode 100644
index 0000000..6f7a6f8
--- /dev/null
+++ b/core/java/android/hardware/hdmi/IHdmiCecSettingChangeListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 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
+ * CEC setting change.
+ *
+ * @hide
+ */
+oneway interface IHdmiCecSettingChangeListener {
+    void onChange(in String setting);
+}
diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
index 65bd856..d7329e0 100644
--- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl
+++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
@@ -18,6 +18,7 @@
 
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.hdmi.IHdmiCecSettingChangeListener;
 import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.hdmi.IHdmiControlStatusChangeListener;
@@ -89,6 +90,8 @@
     boolean isHdmiCecVolumeControlEnabled();
     void reportAudioStatus(int deviceType, int volume, int maxVolume, boolean isMute);
     void setSystemAudioModeOnForAudioOnlySource();
+    void addCecSettingChangeListener(String name, IHdmiCecSettingChangeListener listener);
+    void removeCecSettingChangeListener(String name, IHdmiCecSettingChangeListener listener);
     List<String> getUserCecSettings();
     List<String> getAllowedCecSettingStringValues(String name);
     int[] getAllowedCecSettingIntValues(String name);
diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
index 9531181d..6019b90 100644
--- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
+++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
@@ -395,6 +395,16 @@
         }
 
         @Override
+        public void addCecSettingChangeListener(String name,
+                IHdmiCecSettingChangeListener listener) {
+        }
+
+        @Override
+        public void removeCecSettingChangeListener(String name,
+                IHdmiCecSettingChangeListener listener) {
+        }
+
+        @Override
         public int[] getAllowedCecSettingIntValues(String name) {
             return new int[0];
         }
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 195d506..33f1fb4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -40,6 +40,7 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiHotplugEvent;
 import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.hdmi.IHdmiCecSettingChangeListener;
 import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.hdmi.IHdmiControlService;
@@ -74,6 +75,7 @@
 import android.provider.Settings.Global;
 import android.sysprop.HdmiProperties;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Slog;
 import android.util.SparseArray;
 
@@ -308,6 +310,11 @@
     private final ArrayList<VendorCommandListenerRecord> mVendorCommandListenerRecords =
             new ArrayList<>();
 
+    // List of records for CEC setting change listener to handle the caller killed in action.
+    @GuardedBy("mLock")
+    private final ArrayMap<String, RemoteCallbackList<IHdmiCecSettingChangeListener>>
+            mHdmiCecSettingChangeListenerRecords = new ArrayMap<>();
+
     @GuardedBy("mLock")
     private InputChangeListenerRecord mInputChangeListenerRecord;
 
@@ -2222,6 +2229,20 @@
         }
 
         @Override
+        public void addCecSettingChangeListener(String name,
+                final IHdmiCecSettingChangeListener listener) {
+            enforceAccessPermission();
+            HdmiControlService.this.addCecSettingChangeListener(name, listener);
+        }
+
+        @Override
+        public void removeCecSettingChangeListener(String name,
+                final IHdmiCecSettingChangeListener listener) {
+            enforceAccessPermission();
+            HdmiControlService.this.removeCecSettingChangeListener(name, listener);
+        }
+
+        @Override
         public List<String> getUserCecSettings() {
             enforceAccessPermission();
             long token = Binder.clearCallingIdentity();
@@ -3450,4 +3471,53 @@
     protected HdmiCecConfig getHdmiCecConfig() {
         return mHdmiCecConfig;
     }
+
+    private HdmiCecConfig.SettingChangeListener mSettingChangeListener =
+            new HdmiCecConfig.SettingChangeListener() {
+                @Override
+                public void onChange(String name) {
+                    synchronized (mLock) {
+                        if (!mHdmiCecSettingChangeListenerRecords.containsKey(name)) {
+                            return;
+                        }
+                        mHdmiCecSettingChangeListenerRecords.get(name).broadcast(listener -> {
+                            invokeCecSettingChangeListenerLocked(name, listener);
+                        });
+                    }
+                }
+            };
+
+    private void addCecSettingChangeListener(String name,
+            final IHdmiCecSettingChangeListener listener) {
+        synchronized (mLock) {
+            if (!mHdmiCecSettingChangeListenerRecords.containsKey(name)) {
+                mHdmiCecSettingChangeListenerRecords.put(name, new RemoteCallbackList<>());
+                mHdmiCecConfig.registerChangeListener(name, mSettingChangeListener);
+            }
+            mHdmiCecSettingChangeListenerRecords.get(name).register(listener);
+        }
+    }
+
+    private void removeCecSettingChangeListener(String name,
+            final IHdmiCecSettingChangeListener listener) {
+        synchronized (mLock) {
+            if (!mHdmiCecSettingChangeListenerRecords.containsKey(name)) {
+                return;
+            }
+            mHdmiCecSettingChangeListenerRecords.get(name).unregister(listener);
+            if (mHdmiCecSettingChangeListenerRecords.get(name).getRegisteredCallbackCount() == 0) {
+                mHdmiCecSettingChangeListenerRecords.remove(name);
+                mHdmiCecConfig.removeChangeListener(name, mSettingChangeListener);
+            }
+        }
+    }
+
+    private void invokeCecSettingChangeListenerLocked(String name,
+            final IHdmiCecSettingChangeListener listener) {
+        try {
+            listener.onChange(name);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to report setting change", e);
+        }
+    }
 }