Merge "Revert "Revert "[CEC Configuration] Add listener support to the HdmiCecConfig"""
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
index 98d130f..a759629 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
@@ -22,12 +22,19 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringDef;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.database.ContentObserver;
 import android.hardware.hdmi.HdmiControlManager;
+import android.net.Uri;
 import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.provider.Settings.Global;
+import android.util.ArrayMap;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -88,6 +95,23 @@
     @Nullable private final CecSettings mProductConfig;
     @Nullable private final CecSettings mVendorOverride;
 
+    private final ArrayMap<Setting, Set<SettingChangeListener>>
+            mSettingChangeListeners = new ArrayMap<>();
+
+    private SettingsObserver mSettingsObserver;
+
+    /**
+     * Listener used to get notifications when value of a setting changes.
+     */
+    public interface SettingChangeListener {
+        /**
+         * Called when value of a setting changes.
+         *
+         * @param setting name of a CEC setting that changed
+         */
+        void onChange(@NonNull @CecSettingName String setting);
+    }
+
     /**
      * Setting storage input/output helper class.
      */
@@ -159,6 +183,18 @@
         }
     }
 
+    private class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            String setting = uri.getLastPathSegment();
+            HdmiCecConfig.this.notifyGlobalSettingChanged(setting);
+        }
+    }
+
     @VisibleForTesting
     HdmiCecConfig(@NonNull Context context,
                   @NonNull StorageAdapter storageAdapter,
@@ -311,6 +347,7 @@
         } else if (storage == STORAGE_SHARED_PREFS) {
             Slog.d(TAG, "Setting '" + storageKey + "' shared pref.");
             mStorageAdapter.storeSharedPref(storageKey, value);
+            notifySettingChanged(setting);
         }
     }
 
@@ -318,6 +355,103 @@
         return Integer.decode(value.getIntValue());
     }
 
+    private void notifyGlobalSettingChanged(String setting) {
+        switch (setting) {
+            case Global.HDMI_CONTROL_ENABLED:
+                notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED);
+                break;
+            case Global.HDMI_CEC_VERSION:
+                notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION);
+                break;
+            case Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP:
+                notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_SEND_STANDBY_ON_SLEEP);
+                break;
+        }
+    }
+
+    private void notifySettingChanged(@NonNull @CecSettingName String name) {
+        Setting setting = getSetting(name);
+        if (setting == null) {
+            throw new IllegalArgumentException("Setting '" + name + "' does not exist.");
+        }
+        notifySettingChanged(setting);
+    }
+
+    private void notifySettingChanged(@NonNull Setting setting) {
+        Set<SettingChangeListener> listeners = mSettingChangeListeners.get(setting);
+        if (listeners == null) {
+            return;  // No listeners registered, do nothing.
+        }
+        for (SettingChangeListener listener: listeners) {
+            listener.onChange(setting.getName());
+        }
+    }
+
+    /**
+     * This method registers Global Setting change observer.
+     * Needs to be called once after initialization of HdmiCecConfig.
+     */
+    public void registerGlobalSettingsObserver(Looper looper) {
+        Handler handler = new Handler(looper);
+        mSettingsObserver = new SettingsObserver(handler);
+        ContentResolver resolver = mContext.getContentResolver();
+        String[] settings = new String[] {
+                Global.HDMI_CONTROL_ENABLED,
+                Global.HDMI_CEC_VERSION,
+                Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP,
+        };
+        for (String setting: settings) {
+            resolver.registerContentObserver(Global.getUriFor(setting), false,
+                                             mSettingsObserver, UserHandle.USER_ALL);
+        }
+    }
+
+    /**
+     * This method unregisters Global Setting change observer.
+     */
+    public void unregisterGlobalSettingsObserver() {
+        ContentResolver resolver = mContext.getContentResolver();
+        resolver.unregisterContentObserver(mSettingsObserver);
+    }
+
+    /**
+     * Register change listener for a given setting name.
+     */
+    public void registerChangeListener(@NonNull @CecSettingName String name,
+                                       SettingChangeListener listener) {
+        Setting setting = getSetting(name);
+        if (setting == null) {
+            throw new IllegalArgumentException("Setting '" + name + "' does not exist.");
+        }
+        @Storage int storage = getStorage(setting);
+        if (storage != STORAGE_GLOBAL_SETTINGS && storage != STORAGE_SHARED_PREFS) {
+            throw new IllegalArgumentException("Change listeners for setting '" + name
+                    + "' not supported.");
+        }
+        if (!mSettingChangeListeners.containsKey(setting)) {
+            mSettingChangeListeners.put(setting, new HashSet<>());
+        }
+        mSettingChangeListeners.get(setting).add(listener);
+    }
+
+    /**
+     * Remove change listener for a given setting name.
+     */
+    public void removeChangeListener(@NonNull @CecSettingName String name,
+                                     SettingChangeListener listener) {
+        Setting setting = getSetting(name);
+        if (setting == null) {
+            throw new IllegalArgumentException("Setting '" + name + "' does not exist.");
+        }
+        if (mSettingChangeListeners.containsKey(setting)) {
+            Set<SettingChangeListener> listeners = mSettingChangeListeners.get(setting);
+            listeners.remove(listener);
+            if (listeners.isEmpty()) {
+                mSettingChangeListeners.remove(setting);
+            }
+        }
+    }
+
     /**
      * Returns a list of all settings based on the XML metadata.
      */
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index a1d13e9..7617b71 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -496,6 +496,7 @@
         if (mMessageValidator == null) {
             mMessageValidator = new HdmiCecMessageValidator(this);
         }
+        mHdmiCecConfig.registerGlobalSettingsObserver(mIoLooper);
     }
 
     private void bootCompleted() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
index 777713e..ef39e7e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
@@ -18,13 +18,17 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
 
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.hdmi.HdmiControlManager;
+import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings.Global;
 
@@ -38,15 +42,23 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 @SmallTest
 @Presubmit
 @RunWith(JUnit4.class)
 public final class HdmiCecConfigTest {
     private static final String TAG = "HdmiCecConfigTest";
 
+    private static final int TIMEOUT_CONTENT_CHANGE_SEC = 4;
+
+    private final TestLooper mTestLooper = new TestLooper();
+
     private Context mContext;
 
     @Mock private HdmiCecConfig.StorageAdapter mStorageAdapter;
+    @Mock private HdmiCecConfig.SettingChangeListener mSettingChangeListener;
 
     @Before
     public void setUp() throws Exception {
@@ -1019,4 +1031,124 @@
                 HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING,
                 Integer.toString(HdmiControlManager.SYSTEM_AUDIO_MODE_MUTING_DISABLED));
     }
+
+    @Test
+    public void registerChangeListener_SharedPref_BasicSanity() {
+        HdmiCecConfig hdmiCecConfig = HdmiCecConfig.createFromStrings(
+                mContext, mStorageAdapter,
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+                        + "<cec-settings>"
+                        + "  <setting name=\"system_audio_mode_muting\""
+                        + "           value-type=\"int\""
+                        + "           user-configurable=\"true\">"
+                        + "    <allowed-values>"
+                        + "      <value int-value=\"0\" />"
+                        + "      <value int-value=\"1\" />"
+                        + "    </allowed-values>"
+                        + "    <default-value int-value=\"1\" />"
+                        + "  </setting>"
+                        + "</cec-settings>", null);
+        hdmiCecConfig.registerChangeListener(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING,
+                mSettingChangeListener);
+        hdmiCecConfig.setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING,
+                HdmiControlManager.SYSTEM_AUDIO_MODE_MUTING_DISABLED);
+        verify(mSettingChangeListener).onChange(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING);
+    }
+
+    @Test
+    public void removeChangeListener_SharedPref_BasicSanity() {
+        HdmiCecConfig hdmiCecConfig = HdmiCecConfig.createFromStrings(
+                mContext, mStorageAdapter,
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+                        + "<cec-settings>"
+                        + "  <setting name=\"system_audio_mode_muting\""
+                        + "           value-type=\"int\""
+                        + "           user-configurable=\"true\">"
+                        + "    <allowed-values>"
+                        + "      <value int-value=\"0\" />"
+                        + "      <value int-value=\"1\" />"
+                        + "    </allowed-values>"
+                        + "    <default-value int-value=\"1\" />"
+                        + "  </setting>"
+                        + "</cec-settings>", null);
+        hdmiCecConfig.registerChangeListener(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING,
+                mSettingChangeListener);
+        hdmiCecConfig.removeChangeListener(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING,
+                mSettingChangeListener);
+        hdmiCecConfig.setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING,
+                HdmiControlManager.SYSTEM_AUDIO_MODE_MUTING_DISABLED);
+        verify(mSettingChangeListener, never()).onChange(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING);
+    }
+
+    /**
+     * Externally modified Global Settings still need to be supported. This test verifies that
+     * setting change notification is being forwarded to listeners registered via HdmiCecConfig.
+     */
+    @Test
+    public void globalSettingObserver_BasicSanity() throws Exception {
+        CountDownLatch notifyLatch = new CountDownLatch(1);
+        // Get current value of the setting in the system.
+        String originalValue = Global.getString(mContext.getContentResolver(),
+                Global.HDMI_CONTROL_ENABLED);
+        try {
+            HdmiCecConfig hdmiCecConfig = HdmiCecConfig.createFromStrings(
+                    mContext, mStorageAdapter,
+                    "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+                            + "<cec-settings>"
+                            + "  <setting name=\"hdmi_cec_enabled\""
+                            + "           value-type=\"int\""
+                            + "           user-configurable=\"true\">"
+                            + "    <allowed-values>"
+                            + "      <value int-value=\"0\" />"
+                            + "      <value int-value=\"1\" />"
+                            + "    </allowed-values>"
+                            + "    <default-value int-value=\"1\" />"
+                            + "  </setting>"
+                            + "</cec-settings>", null);
+            hdmiCecConfig.registerGlobalSettingsObserver(mTestLooper.getLooper());
+            HdmiCecConfig.SettingChangeListener latchUpdateListener =
+                    new HdmiCecConfig.SettingChangeListener() {
+                        @Override
+                        public void onChange(
+                                @NonNull @HdmiControlManager.CecSettingName String setting) {
+                            notifyLatch.countDown();
+                            assertThat(setting).isEqualTo(
+                                    HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED);
+                        }
+                    };
+            hdmiCecConfig.registerChangeListener(
+                    HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+                    latchUpdateListener);
+            mTestLooper.dispatchAll();
+
+            // Flip the value of the setting.
+            String valueToSet = ((originalValue == null || originalValue.equals("1")) ? "0" : "1");
+            Global.putString(mContext.getContentResolver(), Global.HDMI_CONTROL_ENABLED,
+                    valueToSet);
+            assertThat(Global.getString(mContext.getContentResolver(),
+                    Global.HDMI_CONTROL_ENABLED)).isEqualTo(valueToSet);
+
+            // Write Setting a 2nd time as the listener doesn't always trigger on the first write
+            // in the test.
+            Global.putString(mContext.getContentResolver(), Global.HDMI_CONTROL_ENABLED,
+                    valueToSet);
+            mTestLooper.dispatchAll();
+
+            if (!notifyLatch.await(TIMEOUT_CONTENT_CHANGE_SEC, TimeUnit.SECONDS)) {
+                fail("Timed out waiting for the notify callback");
+            }
+            hdmiCecConfig.unregisterGlobalSettingsObserver();
+        } finally {
+            // Restore the previous value of the setting in the system.
+            Global.putString(mContext.getContentResolver(), Global.HDMI_CONTROL_ENABLED,
+                    originalValue);
+        }
+    }
 }