diff options
3 files changed, 267 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java index 98d130f1ef69..a759629cf3d9 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.IntDef; 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 @@ public class HdmiCecConfig { @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 @@ public class HdmiCecConfig { } } + 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 @@ public class HdmiCecConfig { } else if (storage == STORAGE_SHARED_PREFS) { Slog.d(TAG, "Setting '" + storageKey + "' shared pref."); mStorageAdapter.storeSharedPref(storageKey, value); + notifySettingChanged(setting); } } @@ -318,6 +355,103 @@ public class HdmiCecConfig { 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 a1d13e974019..7617b71703f1 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 @@ public class HdmiControlService extends SystemService { 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 777713e4ed5a..ef39e7ec8e35 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 @@ package com.android.server.hdmi; 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.junit.runners.JUnit4; 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 @@ public final class HdmiCecConfigTest { 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); + } + } } |