From 7ddbb62ccde54326eb2178d604372c2d9cfc13cc Mon Sep 17 00:00:00 2001 From: Marvin Ramin Date: Fri, 24 Apr 2020 16:15:48 +0200 Subject: [PATCH] CEC: Add listener for HDMI CEC volume control feature Adds a listener to receive updates to the state of the HDMI CEC volume control features. Interested parties can register and unregister to get notified about state updates which are sent on every change to the value. Test: atest HdmiControlServiceTest Bug: 152018314 Change-Id: I342d748114bae99b3c3f236502d73bfeac9e9ac5 Merged-In: I342d748114bae99b3c3f236502d73bfeac9e9ac5 --- .../hardware/hdmi/HdmiControlManager.java | 93 +++++++++++++++++++ .../IHdmiCecVolumeControlFeatureListener.aidl | 32 +++++++ .../hardware/hdmi/IHdmiControlService.aidl | 3 + .../hdmi/HdmiAudioSystemClientTest.java | 10 ++ .../server/hdmi/HdmiControlService.java | 63 +++++++++++++ .../server/hdmi/HdmiControlServiceTest.java | 86 +++++++++++++++++ 6 files changed, 287 insertions(+) create mode 100644 core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java index 6bc962b675764..208406566e52b 100644 --- a/core/java/android/hardware/hdmi/HdmiControlManager.java +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -18,6 +18,7 @@ package android.hardware.hdmi; import static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,6 +31,7 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Binder; import android.os.RemoteException; import android.os.SystemProperties; import android.util.ArrayMap; @@ -40,6 +42,7 @@ import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.Executor; /** * The {@link HdmiControlManager} class is used to send HDMI control messages @@ -817,6 +820,24 @@ public final class HdmiControlManager { private final ArrayMap mHdmiControlStatusChangeListeners = new ArrayMap<>(); + /** + * Listener used to get the status of the HDMI CEC volume control feature (enabled/disabled). + * @hide + */ + public interface HdmiCecVolumeControlFeatureListener { + /** + * Called when the HDMI Control (CEC) volume control feature is enabled/disabled. + * + * @param enabled status of HDMI CEC volume control feature + * @see {@link HdmiControlManager#setHdmiCecVolumeControlEnabled(boolean)} ()} + **/ + void onHdmiCecVolumeControlFeature(boolean enabled); + } + + private final ArrayMap + mHdmiCecVolumeControlFeatureListeners = new ArrayMap<>(); + /** * Listener used to get vendor-specific commands. */ @@ -979,4 +1000,76 @@ public final class HdmiControlManager { }; } + /** + * Adds a listener to get informed of changes to the state of the HDMI CEC volume control + * feature. + * + * Upon adding a listener, the current state of the HDMI CEC volume control feature will be + * sent immediately. + * + *

To stop getting the notification, + * use {@link #removeHdmiCecVolumeControlFeatureListener(HdmiCecVolumeControlFeatureListener)}. + * + * @param listener {@link HdmiCecVolumeControlFeatureListener} instance + * @hide + * @see #removeHdmiCecVolumeControlFeatureListener(HdmiCecVolumeControlFeatureListener) + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void addHdmiCecVolumeControlFeatureListener(@NonNull @CallbackExecutor Executor executor, + @NonNull HdmiCecVolumeControlFeatureListener listener) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + if (mHdmiCecVolumeControlFeatureListeners.containsKey(listener)) { + Log.e(TAG, "listener is already registered"); + return; + } + IHdmiCecVolumeControlFeatureListener wrappedListener = + createHdmiCecVolumeControlFeatureListenerWrapper(executor, listener); + mHdmiCecVolumeControlFeatureListeners.put(listener, wrappedListener); + try { + mService.addHdmiCecVolumeControlFeatureListener(wrappedListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes a listener to stop getting informed of changes to the state of the HDMI CEC volume + * control feature. + * + * @param listener {@link HdmiCecVolumeControlFeatureListener} instance to be removed + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void removeHdmiCecVolumeControlFeatureListener( + HdmiCecVolumeControlFeatureListener listener) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + IHdmiCecVolumeControlFeatureListener wrappedListener = + mHdmiCecVolumeControlFeatureListeners.remove(listener); + if (wrappedListener == null) { + Log.e(TAG, "tried to remove not-registered listener"); + return; + } + try { + mService.removeHdmiCecVolumeControlFeatureListener(wrappedListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private IHdmiCecVolumeControlFeatureListener createHdmiCecVolumeControlFeatureListenerWrapper( + Executor executor, final HdmiCecVolumeControlFeatureListener listener) { + return new android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener.Stub() { + @Override + public void onHdmiCecVolumeControlFeature(boolean enabled) { + Binder.clearCallingIdentity(); + executor.execute(() -> listener.onHdmiCecVolumeControlFeature(enabled)); + } + }; + } } diff --git a/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl b/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl new file mode 100644 index 0000000000000..873438bb1d20a --- /dev/null +++ b/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * Listener used to get the status of the HDMI CEC volume control feature (enabled/disabled). + * @hide + */ +oneway interface IHdmiCecVolumeControlFeatureListener { + + /** + * Called when the HDMI Control (CEC) volume control feature is enabled/disabled. + * + * @param enabled status of HDMI CEC volume control feature + * @see {@link HdmiControlManager#setHdmiCecVolumeControlEnabled(boolean)} ()} + **/ + void onHdmiCecVolumeControlFeature(boolean enabled); +} diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl index 3582a927ff469..4c724ef62ea98 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.IHdmiCecVolumeControlFeatureListener; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.hdmi.IHdmiControlStatusChangeListener; import android.hardware.hdmi.IHdmiDeviceEventListener; @@ -44,6 +45,8 @@ interface IHdmiControlService { void queryDisplayStatus(IHdmiControlCallback callback); void addHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener); void removeHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener); + void addHdmiCecVolumeControlFeatureListener(IHdmiCecVolumeControlFeatureListener listener); + void removeHdmiCecVolumeControlFeatureListener(IHdmiCecVolumeControlFeatureListener listener); void addHotplugEventListener(IHdmiHotplugEventListener listener); void removeHotplugEventListener(IHdmiHotplugEventListener listener); void addDeviceEventListener(IHdmiDeviceEventListener listener); diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java index 7cd2f3b4c2ab2..a4f2065866256 100644 --- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java +++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java @@ -363,6 +363,16 @@ public class HdmiAudioSystemClientTest { public boolean isHdmiCecVolumeControlEnabled() { return true; } + + @Override + public void addHdmiCecVolumeControlFeatureListener( + IHdmiCecVolumeControlFeatureListener listener) { + } + + @Override + public void removeHdmiCecVolumeControlFeatureListener( + IHdmiCecVolumeControlFeatureListener listener) { + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 53f9ebcbd8ddf..8411e82e5c38b 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.IHdmiCecVolumeControlFeatureListener; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.hdmi.IHdmiControlService; import android.hardware.hdmi.IHdmiControlStatusChangeListener; @@ -63,6 +64,7 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; @@ -268,6 +270,11 @@ public class HdmiControlService extends SystemService { private final ArrayList mHdmiControlStatusChangeListenerRecords = new ArrayList<>(); + // List of records for HDMI control volume control status change listener for death monitoring. + @GuardedBy("mLock") + private final RemoteCallbackList + mHdmiCecVolumeControlFeatureListenerRecords = new RemoteCallbackList<>(); + // List of records for hotplug event listener to handle the the caller killed in action. @GuardedBy("mLock") private final ArrayList mHotplugEventListenerRecords = @@ -1813,6 +1820,21 @@ public class HdmiControlService extends SystemService { HdmiControlService.this.removeHdmiControlStatusChangeListener(listener); } + @Override + public void addHdmiCecVolumeControlFeatureListener( + final IHdmiCecVolumeControlFeatureListener listener) { + enforceAccessPermission(); + HdmiControlService.this.addHdmiCecVolumeControlFeatureListener(listener); + } + + @Override + public void removeHdmiCecVolumeControlFeatureListener( + final IHdmiCecVolumeControlFeatureListener listener) { + enforceAccessPermission(); + HdmiControlService.this.removeHdmiControlVolumeControlStatusChangeListener(listener); + } + + @Override public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { enforceAccessPermission(); @@ -2409,6 +2431,33 @@ public class HdmiControlService extends SystemService { } } + @VisibleForTesting + void addHdmiCecVolumeControlFeatureListener( + final IHdmiCecVolumeControlFeatureListener listener) { + mHdmiCecVolumeControlFeatureListenerRecords.register(listener); + + runOnServiceThread(new Runnable() { + @Override + public void run() { + // Return the current status of mHdmiCecVolumeControlEnabled; + synchronized (mLock) { + try { + listener.onHdmiCecVolumeControlFeature(mHdmiCecVolumeControlEnabled); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report HdmiControlVolumeControlStatusChange: " + + mHdmiCecVolumeControlEnabled, e); + } + } + } + }); + } + + @VisibleForTesting + void removeHdmiControlVolumeControlStatusChangeListener( + final IHdmiCecVolumeControlFeatureListener listener) { + mHdmiCecVolumeControlFeatureListenerRecords.unregister(listener); + } + private void addHotplugEventListener(final IHdmiHotplugEventListener listener) { final HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); try { @@ -2682,6 +2731,19 @@ public class HdmiControlService extends SystemService { } } + private void announceHdmiCecVolumeControlFeatureChange(boolean isEnabled) { + assertRunOnServiceThread(); + mHdmiCecVolumeControlFeatureListenerRecords.broadcast(listener -> { + try { + listener.onHdmiCecVolumeControlFeature(isEnabled); + } catch (RemoteException e) { + Slog.e(TAG, + "Failed to report HdmiControlVolumeControlStatusChange: " + + isEnabled); + } + }); + } + public HdmiCecLocalDeviceTv tv() { return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiDeviceInfo.DEVICE_TV); } @@ -3027,6 +3089,7 @@ public class HdmiControlService extends SystemService { isHdmiCecVolumeControlEnabled); } } + announceHdmiCecVolumeControlFeatureChange(isHdmiCecVolumeControlEnabled); } boolean isHdmiCecVolumeControlEnabled() { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index 7af7a23b1ef63..c34b8e19a41d1 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiPortInfo; +import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener; import android.os.IPowerManager; import android.os.IThermalService; import android.os.Looper; @@ -261,4 +262,89 @@ public class HdmiControlServiceTest { mHdmiControlService.setHdmiCecVolumeControlEnabled(true); assertThat(mHdmiControlService.isHdmiCecVolumeControlEnabled()).isTrue(); } + + @Test + public void addHdmiCecVolumeControlFeatureListener_emitsCurrentState_enabled() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isTrue(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_emitsCurrentState_disabled() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isFalse(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_notifiesStateUpdate() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isTrue(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_honorsUnregistration() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback); + mTestLooper.dispatchAll(); + + mHdmiControlService.removeHdmiControlVolumeControlStatusChangeListener(callback); + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + mTestLooper.dispatchAll(); + + assertThat(callback.mCallbackReceived).isTrue(); + assertThat(callback.mVolumeControlEnabled).isFalse(); + } + + @Test + public void addHdmiCecVolumeControlFeatureListener_notifiesStateUpdate_multiple() { + mHdmiControlService.setHdmiCecVolumeControlEnabled(false); + VolumeControlFeatureCallback callback1 = new VolumeControlFeatureCallback(); + VolumeControlFeatureCallback callback2 = new VolumeControlFeatureCallback(); + + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback1); + mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback2); + + + mHdmiControlService.setHdmiCecVolumeControlEnabled(true); + mTestLooper.dispatchAll(); + + assertThat(callback1.mCallbackReceived).isTrue(); + assertThat(callback2.mCallbackReceived).isTrue(); + assertThat(callback1.mVolumeControlEnabled).isTrue(); + assertThat(callback2.mVolumeControlEnabled).isTrue(); + } + + private static class VolumeControlFeatureCallback extends + IHdmiCecVolumeControlFeatureListener.Stub { + boolean mCallbackReceived = false; + boolean mVolumeControlEnabled = false; + + @Override + public void onHdmiCecVolumeControlFeature(boolean enabled) throws RemoteException { + this.mCallbackReceived = true; + this.mVolumeControlEnabled = enabled; + } + } }