From b4fd0e85768625e27a9e97ee7c82a69a96ce268a Mon Sep 17 00:00:00 2001 From: Michal Olech Date: Fri, 9 Oct 2020 01:18:37 +0200 Subject: [PATCH] [CEC Configuration] Add CEC Configuration APIs Bug: 166426337 Bug: 169144568 Test: atest HdmiCecConfigTest Change-Id: I7f109027103e73955d09b251a7a94aee9beded67 --- .../hardware/hdmi/HdmiControlManager.java | 338 +++++++++++++++- .../hdmi/HdmiControlServiceWrapper.java | 39 ++ .../hardware/hdmi/IHdmiControlService.aidl | 4 + .../hdmi/HdmiAudioSystemClientTest.java | 20 + .../android/server/hdmi/HdmiCecConfig.java | 320 +++++++++++++++ .../server/hdmi/HdmiControlService.java | 51 +++ .../server/hdmi/HdmiCecConfigTest.java | 377 ++++++++++++++++++ 7 files changed, 1147 insertions(+), 2 deletions(-) create mode 100644 services/core/java/com/android/server/hdmi/HdmiCecConfig.java create mode 100644 services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java index 5b186c78df54c..9471e49d4090a 100644 --- a/core/java/android/hardware/hdmi/HdmiControlManager.java +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -319,6 +319,29 @@ public final class HdmiControlManager { /** The HdmiControlService will be disabled to standby. */ public static final int CONTROL_STATE_CHANGED_REASON_STANDBY = 3; + // -- Whether the HDMI CEC is enabled or disabled. + /** + * HDMI CEC enabled. + * + * @hide + */ + public static final String HDMI_CEC_CONTROL_ENABLED = "1"; + /** + * HDMI CEC disabled. + * + * @hide + */ + public static final String HDMI_CEC_CONTROL_DISABLED = "0"; + /** + * @hide + */ + @StringDef({ + HDMI_CEC_CONTROL_ENABLED, + HDMI_CEC_CONTROL_DISABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface HdmiCecControl {} + // -- Which devices the playback device can send a message to upon going to sleep. /** * Send to TV only. @@ -347,8 +370,90 @@ public final class HdmiControlManager { SEND_STANDBY_ON_SLEEP_NONE }) @Retention(RetentionPolicy.SOURCE) - public @interface StandbyBehavior { - } + public @interface StandbyBehavior {} + + // -- Which power state action should be taken when Active Source is lost. + /** + * No action to be taken. + * + * @hide + */ + public static final String POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_NONE = "none"; + /** + * Go to standby immediately. + * + * @hide + */ + public static final String POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW = "standby_now"; + /** + * @hide + */ + @StringDef({ + POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_NONE, + POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ActiveSourceLostBehavior {} + + // -- Whether System Audio Mode muting is enabled or disabled. + /** + * System Audio Mode muting enabled. + * + * @hide + */ + public static final String SYSTEM_AUDIO_MODE_MUTING_ENABLED = "1"; + /** + * System Audio Mode muting disabled. + * + * @hide + */ + public static final String SYSTEM_AUDIO_MODE_MUTING_DISABLED = "0"; + /** + * @hide + */ + @StringDef({ + SYSTEM_AUDIO_MODE_MUTING_ENABLED, + SYSTEM_AUDIO_MODE_MUTING_DISABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SystemAudioModeMuting {} + + // -- Settings available in the CEC Configuration. + /** + * Name of a setting deciding whether the CEC is enabled. + * + * @hide + */ + public static final String SETTING_NAME_HDMI_CEC_ENABLED = "hdmi_cec_enabled"; + /** + * Name of a setting deciding on the Standby message behaviour on sleep. + * + * @hide + */ + public static final String SETTING_NAME_SEND_STANDBY_ON_SLEEP = "send_standby_on_sleep"; + /** + * Name of a setting deciding on power state action when losing Active Source. + * + * @hide + */ + public static final String SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST = + "power_state_change_on_active_source_lost"; + /** + * Name of a setting deciding whether System Audio Muting is allowed. + * + * @hide + */ + public static final String SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING = "system_audio_mode_muting"; + /** + * @hide + */ + @StringDef({ + SETTING_NAME_HDMI_CEC_ENABLED, + SETTING_NAME_SEND_STANDBY_ON_SLEEP, + SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, + SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING, + }) + public @interface SettingName {} // True if we have a logical device of type playback hosted in the system. private final boolean mHasPlaybackDevice; @@ -1186,4 +1291,233 @@ public final class HdmiControlManager { } }; } + + /** + * Get a set of user-modifiable settings. + * + * @return a set of user-modifiable settings. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @NonNull + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public List getAvailableCecSettings() { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + return mService.getAvailableCecSettings(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get a set of allowed values for a settings. + * + * @param name name of the setting + * @return a set of allowed values for a settings. {@code null} on failure. + * @throws IllegalArgumentException when setting {@code name} does not exist. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @NonNull + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public List getAllowedCecSettingValues(@NonNull String name) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + return mService.getAllowedCecSettingValues(name); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the 'hdmi_cec_enabled' option. + * + * @param value the desired value + * @throws IllegalArgumentException when the new value is not allowed. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void setHdmiCecEnabled(@NonNull @HdmiCecControl String value) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + mService.setCecSettingValue(SETTING_NAME_HDMI_CEC_ENABLED, value); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the value of 'hdmi_cec_enabled' option. + * + * @return the current value. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @NonNull + @HdmiCecControl + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public String getHdmiCecEnabled() { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + return mService.getCecSettingValue(SETTING_NAME_HDMI_CEC_ENABLED); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the 'send_standby_on_sleep' option. + * + * @param value the desired value + * @throws IllegalArgumentException when the new value is not allowed. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void setSendStandbyOnSleep(@NonNull @StandbyBehavior String value) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + mService.setCecSettingValue(SETTING_NAME_SEND_STANDBY_ON_SLEEP, value); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the value of 'send_standby_on_sleep' option. + * + * @return the current value. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @NonNull + @StandbyBehavior + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public String getSendStandbyOnSleep() { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + return mService.getCecSettingValue(SETTING_NAME_SEND_STANDBY_ON_SLEEP); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the 'power_state_change_on_active_source_lost' option. + * + * @param value the desired value + * @throws IllegalArgumentException when the new value is not allowed + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void setPowerStateChangeOnActiveSourceLost( + @NonNull @ActiveSourceLostBehavior String value) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + mService.setCecSettingValue( + SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, value); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the value of 'power_state_change_on_active_source_lost' option. + * + * @return the current value. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @NonNull + @ActiveSourceLostBehavior + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public String getPowerStateChangeOnActiveSourceLost() { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + return mService.getCecSettingValue( + SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the 'system_audio_mode_muting' option. + * + * @param value the desired value + * @throws IllegalArgumentException when the new value is not allowed. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void setSystemAudioModeMuting(@NonNull @SystemAudioModeMuting String value) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + mService.setCecSettingValue(SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING, value); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the value of 'system_audio_mode_muting' option. + * + * @return the current value. + * @throws RuntimeException when the HdmiControlService is not available. + * + * @hide + */ + @NonNull + @SystemAudioModeMuting + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public String getSystemAudioModeMuting() { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + throw new RuntimeException("HdmiControlService is not available"); + } + try { + return mService.getCecSettingValue(SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java index 02896351ea3a0..46a3c4fa4bfa1 100644 --- a/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java +++ b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java @@ -20,6 +20,7 @@ import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.TestApi; +import java.util.ArrayList; import java.util.List; /** @@ -293,6 +294,26 @@ public final class HdmiControlServiceWrapper { IHdmiCecVolumeControlFeatureListener listener) { HdmiControlServiceWrapper.this.removeHdmiCecVolumeControlFeatureListener(listener); } + + @Override + public List getAvailableCecSettings() { + return HdmiControlServiceWrapper.this.getAvailableCecSettings(); + } + + @Override + public List getAllowedCecSettingValues(String name) { + return HdmiControlServiceWrapper.this.getAllowedCecSettingValues(name); + } + + @Override + public String getCecSettingValue(String name) { + return HdmiControlServiceWrapper.this.getCecSettingValue(name); + } + + @Override + public void setCecSettingValue(String name, String value) { + HdmiControlServiceWrapper.this.setCecSettingValue(name, value); + } }; @BinderThread @@ -466,4 +487,22 @@ public final class HdmiControlServiceWrapper { /** @hide */ public void removeHdmiCecVolumeControlFeatureListener( IHdmiCecVolumeControlFeatureListener listener) {} + + /** @hide */ + public List getAvailableCecSettings() { + return new ArrayList<>(); + } + + /** @hide */ + public List getAllowedCecSettingValues(String name) { + return new ArrayList<>(); + } + + /** @hide */ + public String getCecSettingValue(String name) { + return ""; + } + + /** @hide */ + public void setCecSettingValue(String name, String value) {} } diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl index 4c724ef62ea98..3478b238f07fa 100644 --- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl +++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl @@ -87,4 +87,8 @@ interface IHdmiControlService { boolean isHdmiCecVolumeControlEnabled(); void reportAudioStatus(int deviceType, int volume, int maxVolume, boolean isMute); void setSystemAudioModeOnForAudioOnlySource(); + List getAvailableCecSettings(); + List getAllowedCecSettingValues(String name); + String getCecSettingValue(String name); + void setCecSettingValue(String name, String value); } diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java index a4f2065866256..f9140bc4caa4f 100644 --- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java +++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.util.ArrayList; import java.util.List; /** @@ -373,6 +374,25 @@ public class HdmiAudioSystemClientTest { public void removeHdmiCecVolumeControlFeatureListener( IHdmiCecVolumeControlFeatureListener listener) { } + + @Override + public List getAvailableCecSettings() { + return new ArrayList<>(); + } + + @Override + public List getAllowedCecSettingValues(String name) { + return new ArrayList<>(); + } + + @Override + public String getCecSettingValue(String name) { + return ""; + } + + @Override + public void setCecSettingValue(String name, String value) { + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java new file mode 100644 index 0000000000000..393a66eb1063d --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java @@ -0,0 +1,320 @@ +/* + * Copyright 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 com.android.server.hdmi; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.hdmi.HdmiControlManager; +import android.os.Environment; +import android.os.SystemProperties; +import android.provider.Settings.Global; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.hdmi.cec.config.CecSettings; +import com.android.server.hdmi.cec.config.Setting; +import com.android.server.hdmi.cec.config.Value; +import com.android.server.hdmi.cec.config.XmlParser; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.xml.datatype.DatatypeConfigurationException; + +/** + * The {@link HdmiCecConfig} class is used for getting information about + * available HDMI CEC settings. + */ +public class HdmiCecConfig { + private static final String TAG = "HdmiCecConfig"; + + private static final String ETC_DIR = "etc"; + private static final String CONFIG_FILE = "cec_config.xml"; + + @Nullable private CecSettings mProductConfig = null; + @Nullable private CecSettings mVendorOverride = null; + @Nullable private StorageAdapter mStorageAdapter = null; + + @IntDef({ + STORAGE_SYSPROPS, + STORAGE_GLOBAL_SETTINGS, + }) + private @interface Storage {} + + private static final int STORAGE_SYSPROPS = 0; + private static final int STORAGE_GLOBAL_SETTINGS = 1; + + /** + * System property key for Power State Change on Active Source Lost. + */ + public static final String SYSPROP_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST = + "ro.hdmi.cec.source.power_state_change_on_active_source_lost"; + + /** + * System property key for Audio Mode Muting. + */ + public static final String SYSPROP_SYSTEM_AUDIO_MODE_MUTING = + "ro.hdmi.cec.audio.system_audio_mode_muting.enabled"; + + /** + * Setting storage input/output helper class. + */ + public static class StorageAdapter { + /** + * Read the value from a system property. + * Returns the given default value if the system property is not set. + */ + public String retrieveSystemProperty(@NonNull String storageKey, + @NonNull String defaultValue) { + return SystemProperties.get(storageKey, defaultValue); + } + + /** + * Write the value to a system property. + */ + public void storeSystemProperty(@NonNull String storageKey, + @NonNull String value) { + SystemProperties.set(storageKey, value); + } + + /** + * Read the value from a global setting. + * Returns the given default value if the system property is not set. + */ + public String retrieveGlobalSetting(@NonNull Context context, + @NonNull String storageKey, + @NonNull String defaultValue) { + String value = Global.getString(context.getContentResolver(), storageKey); + return value != null ? value : defaultValue; + } + + /** + * Write the value to a global setting. + */ + public void storeGlobalSetting(@NonNull Context context, + @NonNull String storageKey, + @NonNull String value) { + Global.putString(context.getContentResolver(), storageKey, value); + } + } + + @VisibleForTesting + HdmiCecConfig(@Nullable CecSettings productConfig, + @Nullable CecSettings vendorOverride, + @Nullable StorageAdapter storageAdapter) { + mProductConfig = productConfig; + mVendorOverride = vendorOverride; + mStorageAdapter = storageAdapter; + } + + HdmiCecConfig() { + this(readSettingsFromFile(Environment.buildPath(Environment.getProductDirectory(), + ETC_DIR, CONFIG_FILE)), + readSettingsFromFile(Environment.buildPath(Environment.getVendorDirectory(), + ETC_DIR, CONFIG_FILE)), + new StorageAdapter()); + } + + @Nullable + private static CecSettings readSettingsFromFile(@NonNull File file) { + if (!file.exists()) { + return null; + } + if (!file.isFile()) { + Slog.e(TAG, "CEC configuration is not a file: " + file + ", skipping."); + return null; + } + try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { + return XmlParser.read(in); + } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) { + Slog.e(TAG, "Encountered an error while reading/parsing CEC config file: " + file, e); + } + return null; + } + + @Nullable + private Setting getSetting(@NonNull String name) { + if (mVendorOverride != null) { + // First read from the vendor override. + for (Setting setting : mVendorOverride.getSetting()) { + if (setting.getName().equals(name)) { + return setting; + } + } + } + // If not found, try the product config. + for (Setting setting : mProductConfig.getSetting()) { + if (setting.getName().equals(name)) { + return setting; + } + } + return null; + } + + @Storage + private int getStorage(@NonNull Setting setting) { + switch (setting.getName()) { + case HdmiControlManager.SETTING_NAME_HDMI_CEC_ENABLED: + return STORAGE_GLOBAL_SETTINGS; + case HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP: + return STORAGE_GLOBAL_SETTINGS; + case HdmiControlManager.SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST: + return STORAGE_SYSPROPS; + case HdmiControlManager.SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING: + return STORAGE_SYSPROPS; + default: + throw new RuntimeException("Invalid CEC setting '" + setting.getName() + + "' storage."); + } + } + + private String getStorageKey(@NonNull Setting setting) { + switch (setting.getName()) { + case HdmiControlManager.SETTING_NAME_HDMI_CEC_ENABLED: + return Global.HDMI_CONTROL_ENABLED; + case HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP: + return Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP; + case HdmiControlManager.SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST: + return SYSPROP_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST; + case HdmiControlManager.SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING: + return SYSPROP_SYSTEM_AUDIO_MODE_MUTING; + default: + throw new RuntimeException("Invalid CEC setting '" + setting.getName() + + "' storage key."); + } + } + + private String retrieveValue(@NonNull Context context, @NonNull Setting setting) { + @Storage int storage = getStorage(setting); + String storageKey = getStorageKey(setting); + if (storage == STORAGE_SYSPROPS) { + Slog.d(TAG, "Reading '" + storageKey + "' sysprop."); + return mStorageAdapter.retrieveSystemProperty(storageKey, + setting.getDefaultValue().getStringValue()); + } else if (storage == STORAGE_GLOBAL_SETTINGS) { + Slog.d(TAG, "Reading '" + storageKey + "' global setting."); + return mStorageAdapter.retrieveGlobalSetting(context, storageKey, + setting.getDefaultValue().getStringValue()); + } + return null; + } + + private void storeValue(@NonNull Context context, @NonNull Setting setting, + @NonNull String value) { + @Storage int storage = getStorage(setting); + String storageKey = getStorageKey(setting); + if (storage == STORAGE_SYSPROPS) { + Slog.d(TAG, "Setting '" + storageKey + "' sysprop."); + mStorageAdapter.storeSystemProperty(storageKey, value); + } else if (storage == STORAGE_GLOBAL_SETTINGS) { + Slog.d(TAG, "Setting '" + storageKey + "' global setting."); + mStorageAdapter.storeGlobalSetting(context, storageKey, value); + } + } + + /** + * Returns a list of currently available settings based on the XML metadata. + */ + public List getAvailableSettings() { + Set availableSettings = new HashSet(); + // First read from the product config. + for (Setting setting : mProductConfig.getSetting()) { + if (setting.getUserConfigurable()) { + availableSettings.add(setting.getName()); + } + } + if (mVendorOverride != null) { + // Next either add or remove based on the vendor override. + for (Setting setting : mVendorOverride.getSetting()) { + if (setting.getUserConfigurable()) { + availableSettings.add(setting.getName()); + } else { + availableSettings.remove(setting.getName()); + } + } + } + return new ArrayList(availableSettings); + } + + /** + * For a given setting name returns values that are allowed for that setting. + */ + public List getAllowedValues(@NonNull String name) { + Setting setting = getSetting(name); + if (setting == null) { + throw new IllegalArgumentException("Setting '" + name + "' does not exist."); + } + List allowedValues = new ArrayList(); + for (Value allowedValue : setting.getAllowedValues().getValue()) { + allowedValues.add(allowedValue.getStringValue()); + } + return allowedValues; + } + + /** + * For a given setting name returns the default value for that setting. + */ + public String getDefaultValue(@NonNull String name) { + Setting setting = getSetting(name); + if (setting == null) { + throw new IllegalArgumentException("Setting '" + name + "' does not exist."); + } + return getSetting(name).getDefaultValue().getStringValue(); + } + + /** + * For a given setting name returns the current value of that setting. + */ + public String getValue(@NonNull Context context, @NonNull String name) { + Setting setting = getSetting(name); + if (setting == null) { + throw new IllegalArgumentException("Setting '" + name + "' does not exist."); + } + Slog.d(TAG, "Getting CEC setting value '" + name + "'."); + return retrieveValue(context, setting); + } + + /** + * For a given setting name and value sets the current value of that setting. + */ + public void setValue(@NonNull Context context, @NonNull String name, @NonNull String value) { + Setting setting = getSetting(name); + if (setting == null) { + throw new IllegalArgumentException("Setting '" + name + "' does not exist."); + } + if (!setting.getUserConfigurable()) { + throw new IllegalArgumentException("Updating CEC setting '" + name + "' prohibited."); + } + if (!getAllowedValues(name).contains(value)) { + throw new IllegalArgumentException("Invalid CEC setting '" + name + + "' value: '" + value + "'."); + } + Slog.d(TAG, "Updating CEC setting '" + name + "' to '" + value + "'."); + storeValue(context, setting, value); + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 27bd0563cea58..98830cd7e2332 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -187,6 +187,9 @@ public class HdmiControlService extends SystemService { @GuardedBy("mLock") private boolean mHdmiCecVolumeControlEnabled; + // Make sure HdmiCecConfig is instantiated and the XMLs are read. + private final HdmiCecConfig mHdmiCecConfig = new HdmiCecConfig(); + /** * Interface to report send result. */ @@ -2329,6 +2332,50 @@ public class HdmiControlService extends SystemService { pw.decreaseIndent(); } } + + @Override + public List getAvailableCecSettings() { + enforceAccessPermission(); + long token = Binder.clearCallingIdentity(); + try { + return HdmiControlService.this.getHdmiCecConfig().getAvailableSettings(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public List getAllowedCecSettingValues(String name) { + enforceAccessPermission(); + long token = Binder.clearCallingIdentity(); + try { + return HdmiControlService.this.getHdmiCecConfig().getAllowedValues(name); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public String getCecSettingValue(String name) { + enforceAccessPermission(); + long token = Binder.clearCallingIdentity(); + try { + return HdmiControlService.this.getHdmiCecConfig().getValue(getContext(), name); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void setCecSettingValue(String name, String value) { + enforceAccessPermission(); + long token = Binder.clearCallingIdentity(); + try { + HdmiControlService.this.getHdmiCecConfig().setValue(getContext(), name, value); + } finally { + Binder.restoreCallingIdentity(token); + } + } } // Get the source address to send out commands to devices connected to the current device @@ -3389,4 +3436,8 @@ public class HdmiControlService extends SystemService { getContext().sendBroadcastAsUser(intent, UserHandle.ALL, HdmiControlService.PERMISSION); } + + HdmiCecConfig getHdmiCecConfig() { + return mHdmiCecConfig; + } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java new file mode 100644 index 0000000000000..af2a4be95d9dd --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2018 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 com.android.server.hdmi; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; + +import android.content.Context; +import android.hardware.hdmi.HdmiControlManager; +import android.platform.test.annotations.Presubmit; +import android.provider.Settings.Global; +import android.sysprop.HdmiProperties; +import android.util.Slog; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.server.hdmi.cec.config.CecSettings; +import com.android.server.hdmi.cec.config.XmlParser; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import javax.xml.datatype.DatatypeConfigurationException; + +@SmallTest +@Presubmit +@RunWith(JUnit4.class) +public final class HdmiCecConfigTest { + private static final String TAG = "HdmiCecConfigTest"; + + private Context mContext; + + @Mock private HdmiCecConfig.StorageAdapter mStorageAdapter; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = InstrumentationRegistry.getTargetContext(); + } + + @Test + public void getAvailableCecSettings_Empty() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + "", null); + assertThat(hdmiCecConfig.getAvailableSettings()).isEmpty(); + } + + @Test + public void getAvailableCecSettings_OnlyMasterXml() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + assertThat(hdmiCecConfig.getAvailableSettings()) + .containsExactly(HdmiControlManager.SETTING_NAME_HDMI_CEC_ENABLED, + HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP); + } + + @Test + public void getAvailableCecSettings_WithOverride() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""); + assertThat(hdmiCecConfig.getAvailableSettings()) + .containsExactly(HdmiControlManager.SETTING_NAME_HDMI_CEC_ENABLED); + } + + @Test + public void getAllowedValues_InvalidSetting() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + "", null); + assertThrows(IllegalArgumentException.class, + () -> hdmiCecConfig.getAllowedValues("foo")); + } + + @Test + public void getAllowedValues_BasicSanity() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + assertThat(hdmiCecConfig.getAllowedValues( + HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP)) + .containsExactly(HdmiControlManager.SEND_STANDBY_ON_SLEEP_TO_TV, + HdmiControlManager.SEND_STANDBY_ON_SLEEP_BROADCAST, + HdmiControlManager.SEND_STANDBY_ON_SLEEP_NONE); + } + + @Test + public void getDefaultValue_InvalidSetting() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + "", null); + assertThrows(IllegalArgumentException.class, + () -> hdmiCecConfig.getDefaultValue("foo")); + } + + @Test + public void getDefaultValue_BasicSanity() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + assertThat(hdmiCecConfig.getDefaultValue( + HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP)) + .isEqualTo(HdmiControlManager.SEND_STANDBY_ON_SLEEP_TO_TV); + } + + @Test + public void getValue_InvalidSetting() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + "", null); + assertThrows(IllegalArgumentException.class, + () -> hdmiCecConfig.getValue(mContext, "foo")); + } + + @Test + public void getValue_GlobalSetting_BasicSanity() { + when(mStorageAdapter.retrieveGlobalSetting(mContext, + Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP, + HdmiControlManager.SEND_STANDBY_ON_SLEEP_TO_TV)) + .thenReturn(HdmiControlManager.SEND_STANDBY_ON_SLEEP_BROADCAST); + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + assertThat(hdmiCecConfig.getValue(mContext, + HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP)) + .isEqualTo(HdmiControlManager.SEND_STANDBY_ON_SLEEP_BROADCAST); + } + + @Test + public void getValue_SystemProperty_BasicSanity() { + when(mStorageAdapter.retrieveSystemProperty( + HdmiCecConfig.SYSPROP_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, + HdmiProperties.power_state_change_on_active_source_lost_values + .NONE.name().toLowerCase())) + .thenReturn(HdmiProperties.power_state_change_on_active_source_lost_values + .STANDBY_NOW.name().toLowerCase()); + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + assertThat(hdmiCecConfig.getValue(mContext, + HdmiControlManager.SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST)) + .isEqualTo(HdmiProperties.power_state_change_on_active_source_lost_values + .STANDBY_NOW.name().toLowerCase()); + } + + @Test + public void setValue_InvalidSetting() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + "", null); + assertThrows(IllegalArgumentException.class, + () -> hdmiCecConfig.setValue(mContext, "foo", "bar")); + } + + @Test + public void setValue_NotConfigurable() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + assertThrows(IllegalArgumentException.class, + () -> hdmiCecConfig.setValue(mContext, + HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP, + HdmiControlManager.SEND_STANDBY_ON_SLEEP_BROADCAST)); + } + + @Test + public void setValue_InvalidValue() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + assertThrows(IllegalArgumentException.class, + () -> hdmiCecConfig.setValue(mContext, + HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP, + "bar")); + } + + @Test + public void setValue_GlobalSetting_BasicSanity() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + hdmiCecConfig.setValue(mContext, + HdmiControlManager.SETTING_NAME_SEND_STANDBY_ON_SLEEP, + HdmiControlManager.SEND_STANDBY_ON_SLEEP_BROADCAST); + verify(mStorageAdapter).storeGlobalSetting(mContext, + Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP, + HdmiControlManager.SEND_STANDBY_ON_SLEEP_BROADCAST); + } + + @Test + public void setValue_SystemProperty_BasicSanity() { + HdmiCecConfig hdmiCecConfig = createHdmiCecConfig( + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "", null); + hdmiCecConfig.setValue(mContext, + HdmiControlManager.SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, + HdmiProperties.power_state_change_on_active_source_lost_values + .STANDBY_NOW.name().toLowerCase()); + verify(mStorageAdapter).storeSystemProperty( + HdmiCecConfig.SYSPROP_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, + HdmiProperties.power_state_change_on_active_source_lost_values + .STANDBY_NOW.name().toLowerCase()); + } + + private HdmiCecConfig createHdmiCecConfig(String productConfigXml, String vendorOverrideXml) { + CecSettings productConfig = null; + CecSettings vendorOverride = null; + try { + productConfig = XmlParser.read(new ByteArrayInputStream(productConfigXml.getBytes())); + if (vendorOverrideXml != null) { + vendorOverride = XmlParser.read( + new ByteArrayInputStream(vendorOverrideXml.getBytes())); + } + } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) { + Slog.e(TAG, "Encountered an error while reading/parsing CEC config strings", e); + } + return new HdmiCecConfig(productConfig, vendorOverride, mStorageAdapter); + } +}