diff --git a/core/api/system-current.txt b/core/api/system-current.txt index a6dfdac44ff10..9386d23a114e8 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2741,6 +2741,8 @@ package android.hardware.hdmi { } 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 getAllowedCecSettingIntValues(@NonNull String); @@ -2759,6 +2761,7 @@ package android.hardware.hdmi { method @NonNull @RequiresPermission(android.Manifest.permission.HDMI_CEC) public java.util.List 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 @@ package android.hardware.hdmi { 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 38ffc80e3f217..b09eda4ad2b60 100644 --- a/core/java/android/hardware/hdmi/HdmiControlManager.java +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -1371,6 +1371,87 @@ public final class HdmiControlManager { }; } + /** + * 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> + 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. * @@ -1492,6 +1573,53 @@ public final class HdmiControlManager { } } + /** + * Add change listener for global status of HDMI CEC. + * + *

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. + * + *

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. * diff --git a/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java index 3b61911fcde5b..89a7afa894eba 100644 --- a/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java +++ b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java @@ -305,6 +305,18 @@ public final class HdmiControlServiceWrapper { HdmiControlServiceWrapper.this.removeHdmiCecVolumeControlFeatureListener(listener); } + @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 getUserCecSettings() { return HdmiControlServiceWrapper.this.getUserCecSettings(); @@ -521,6 +533,14 @@ public final class HdmiControlServiceWrapper { public void removeHdmiCecVolumeControlFeatureListener( IHdmiCecVolumeControlFeatureListener listener) {} + /** @hide */ + public void addCecSettingChangeListener(String name, + IHdmiCecSettingChangeListener listener) {} + + /** @hide */ + public void removeCecSettingChangeListener(String name, + IHdmiCecSettingChangeListener listener) {} + /** @hide */ public List 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 0000000000000..6f7a6f8b8debc --- /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 65bd8565c8641..d7329e0ba3b50 100644 --- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl +++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl @@ -18,6 +18,7 @@ package android.hardware.hdmi; 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 @@ interface IHdmiControlService { 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 getUserCecSettings(); List 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 9531181d3d292..6019b90ca13bd 100644 --- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java +++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java @@ -394,6 +394,16 @@ public class HdmiAudioSystemClientTest { return new ArrayList<>(); } + @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 195d5068a902a..33f1fb41169df 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.HdmiControlManager; 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.os.UserHandle; 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 @@ public class HdmiControlService extends SystemService { private final ArrayList mVendorCommandListenerRecords = new ArrayList<>(); + // List of records for CEC setting change listener to handle the caller killed in action. + @GuardedBy("mLock") + private final ArrayMap> + mHdmiCecSettingChangeListenerRecords = new ArrayMap<>(); + @GuardedBy("mLock") private InputChangeListenerRecord mInputChangeListenerRecord; @@ -2221,6 +2228,20 @@ public class HdmiControlService extends SystemService { } } + @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 getUserCecSettings() { enforceAccessPermission(); @@ -3450,4 +3471,53 @@ public class HdmiControlService extends SystemService { 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); + } + } }