diff options
6 files changed, 321 insertions, 27 deletions
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java index d05ba799205c..d5dadbff419f 100644 --- a/core/java/android/hardware/hdmi/HdmiControlManager.java +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -685,6 +685,28 @@ public final class HdmiControlManager { mHotplugEventListeners = new ArrayMap<>(); /** + * Listener used to get HDMI Control (CEC) status (enabled/disabled) and the connected display + * status. + * @hide + */ + public interface HdmiControlStatusChangeListener { + /** + * Called when HDMI Control (CEC) is enabled/disabled. + * + * @param isCecEnabled status of HDMI Control + * {@link android.provider.Settings.Global#HDMI_CONTROL_ENABLED}: {@code true} if enabled. + * @param isCecAvailable status of CEC support of the connected display (the TV). + * {@code true} if supported. + * + * Note: Value of isCecAvailable is only valid when isCecEnabled is true. + **/ + void onStatusChange(boolean isCecEnabled, boolean isCecAvailable); + } + + private final ArrayMap<HdmiControlStatusChangeListener, IHdmiControlStatusChangeListener> + mHdmiControlStatusChangeListeners = new ArrayMap<>(); + + /** * Listener used to get vendor-specific commands. */ public interface VendorCommandListener { @@ -777,4 +799,73 @@ public final class HdmiControlManager { } }; } + + /** + * Adds a listener to get informed of {@link HdmiControlStatusChange}. + * + * <p>To stop getting the notification, + * use {@link #removeHdmiControlStatusChangeListener(HdmiControlStatusChangeListener)}. + * + * @param listener {@link HdmiControlStatusChangeListener} instance + * @see HdmiControlManager#removeHdmiControlStatusChangeListener( + * HdmiControlStatusChangeListener) + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void addHdmiControlStatusChangeListener(HdmiControlStatusChangeListener listener) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + if (mHdmiControlStatusChangeListeners.containsKey(listener)) { + Log.e(TAG, "listener is already registered"); + return; + } + IHdmiControlStatusChangeListener wrappedListener = + getHdmiControlStatusChangeListenerWrapper(listener); + mHdmiControlStatusChangeListeners.put(listener, wrappedListener); + try { + mService.addHdmiControlStatusChangeListener(wrappedListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes a listener to stop getting informed of {@link HdmiControlStatusChange}. + * + * @param listener {@link HdmiControlStatusChangeListener} instance to be removed + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.HDMI_CEC) + public void removeHdmiControlStatusChangeListener(HdmiControlStatusChangeListener listener) { + if (mService == null) { + Log.e(TAG, "HdmiControlService is not available"); + return; + } + IHdmiControlStatusChangeListener wrappedListener = + mHdmiControlStatusChangeListeners.remove(listener); + if (wrappedListener == null) { + Log.e(TAG, "tried to remove not-registered listener"); + return; + } + try { + mService.removeHdmiControlStatusChangeListener(wrappedListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private IHdmiControlStatusChangeListener getHdmiControlStatusChangeListenerWrapper( + final HdmiControlStatusChangeListener listener) { + return new IHdmiControlStatusChangeListener.Stub() { + @Override + public void onStatusChange(boolean isCecEnabled, boolean isCecAvailable) { + listener.onStatusChange(isCecEnabled, isCecAvailable); + } + }; + } + } diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl index 95eaf75d6510..a8fed2b03cfc 100644 --- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl +++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl @@ -19,6 +19,7 @@ package android.hardware.hdmi; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiControlCallback; +import android.hardware.hdmi.IHdmiControlStatusChangeListener; import android.hardware.hdmi.IHdmiDeviceEventListener; import android.hardware.hdmi.IHdmiHotplugEventListener; import android.hardware.hdmi.IHdmiInputChangeListener; @@ -41,6 +42,8 @@ interface IHdmiControlService { HdmiDeviceInfo getActiveSource(); void oneTouchPlay(IHdmiControlCallback callback); void queryDisplayStatus(IHdmiControlCallback callback); + void addHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener); + void removeHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener); void addHotplugEventListener(IHdmiHotplugEventListener listener); void removeHotplugEventListener(IHdmiHotplugEventListener listener); void addDeviceEventListener(IHdmiDeviceEventListener listener); diff --git a/core/java/android/hardware/hdmi/IHdmiControlStatusChangeListener.aidl b/core/java/android/hardware/hdmi/IHdmiControlStatusChangeListener.aidl new file mode 100644 index 000000000000..1407821b413c --- /dev/null +++ b/core/java/android/hardware/hdmi/IHdmiControlStatusChangeListener.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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; + +import android.hardware.hdmi.HdmiDeviceInfo; + +/** + * Callback interface definition for HDMI client to get informed of + * the CEC availability change event. + * + * @hide + */ +oneway interface IHdmiControlStatusChangeListener { + + /** + * Called when HDMI Control (CEC) is enabled/disabled. + * + * @param isCecEnabled status of HDMI Control + * {@link android.provider.Settings.Global#HDMI_CONTROL_ENABLED}: {@code true} if enabled. + * @param isCecAvailable status of CEC support of the connected display (the TV). + * {@code true} if supported. + * + * Note: Value of isCecAvailable is only valid when isCecEnabled is true. + **/ + void onStatusChange(boolean isCecEnabled, boolean isCecAvailable); +} diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java index 0a729a98a6a8..2141b815fb3b 100644 --- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java +++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java @@ -194,6 +194,16 @@ public class HdmiAudioSystemClientTest { } @Override + public void addHdmiControlStatusChangeListener( + final IHdmiControlStatusChangeListener listener) { + } + + @Override + public void removeHdmiControlStatusChangeListener( + final IHdmiControlStatusChangeListener listener) { + } + + @Override public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 5bc2261878b6..50b6ced7f81e 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -845,7 +845,12 @@ public class AudioService extends IAudioService.Stub if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_HDMI_CEC)) { synchronized (mHdmiClientLock) { + mHdmiCecSink = false; mHdmiManager = mContext.getSystemService(HdmiControlManager.class); + if (mHdmiManager != null) { + mHdmiManager.addHdmiControlStatusChangeListener( + mHdmiControlStatusChangeListenerCallback); + } mHdmiTvClient = mHdmiManager.getTvClient(); if (mHdmiTvClient != null) { mFixedVolumeDevices &= ~AudioSystem.DEVICE_ALL_HDMI_SYSTEM_AUDIO_AND_SPEAKER; @@ -856,7 +861,6 @@ public class AudioService extends IAudioService.Stub mFixedVolumeDevices &= ~AudioSystem.DEVICE_OUT_HDMI; mFullVolumeDevices |= AudioSystem.DEVICE_OUT_HDMI; } - mHdmiCecSink = false; mHdmiAudioSystemClient = mHdmiManager.getAudioSystemClient(); } } @@ -1113,8 +1117,7 @@ public class AudioService extends IAudioService.Stub checkAddAllFixedVolumeDevices(AudioSystem.DEVICE_OUT_HDMI, caller); synchronized (mHdmiClientLock) { if (mHdmiManager != null && mHdmiPlaybackClient != null) { - mHdmiCecSink = false; - mHdmiPlaybackClient.queryDisplayStatus(mHdmiDisplayStatusCallback); + updateHdmiCecSinkLocked(mHdmiCecSink | false); } } } @@ -1124,7 +1127,7 @@ public class AudioService extends IAudioService.Stub if (isPlatformTelevision()) { synchronized (mHdmiClientLock) { if (mHdmiManager != null) { - mHdmiCecSink = false; + updateHdmiCecSinkLocked(mHdmiCecSink | false); } } } @@ -5774,32 +5777,37 @@ public class AudioService extends IAudioService.Stub // are transformed into key events for the HDMI playback client. //========================================================================================== - private class MyDisplayStatusCallback implements HdmiPlaybackClient.DisplayStatusCallback { - public void onComplete(int status) { - synchronized (mHdmiClientLock) { - if (mHdmiManager != null) { - mHdmiCecSink = (status != HdmiControlManager.POWER_STATUS_UNKNOWN); - // Television devices without CEC service apply software volume on HDMI output - if (mHdmiCecSink) { - if (DEBUG_VOL) { - Log.d(TAG, "CEC sink: setting HDMI as full vol device"); - } - mFullVolumeDevices |= AudioSystem.DEVICE_OUT_HDMI; - } else { - if (DEBUG_VOL) { - Log.d(TAG, "TV, no CEC: setting HDMI as regular vol device"); - } - // Android TV devices without CEC service apply software volume on - // HDMI output - mFullVolumeDevices &= ~AudioSystem.DEVICE_OUT_HDMI; - } - checkAddAllFixedVolumeDevices(AudioSystem.DEVICE_OUT_HDMI, - "HdmiPlaybackClient.DisplayStatusCallback"); - } + @GuardedBy("mHdmiClientLock") + private void updateHdmiCecSinkLocked(boolean hdmiCecSink) { + mHdmiCecSink = hdmiCecSink; + if (mHdmiCecSink) { + if (DEBUG_VOL) { + Log.d(TAG, "CEC sink: setting HDMI as full vol device"); + } + mFullVolumeDevices |= AudioSystem.DEVICE_OUT_HDMI; + } else { + if (DEBUG_VOL) { + Log.d(TAG, "TV, no CEC: setting HDMI as regular vol device"); } + // Android TV devices without CEC service apply software volume on + // HDMI output + mFullVolumeDevices &= ~AudioSystem.DEVICE_OUT_HDMI; } + + checkAddAllFixedVolumeDevices(AudioSystem.DEVICE_OUT_HDMI, + "HdmiPlaybackClient.DisplayStatusCallback"); } + private class MyHdmiControlStatusChangeListenerCallback + implements HdmiControlManager.HdmiControlStatusChangeListener { + public void onStatusChange(boolean isCecEnabled, boolean isCecAvailable) { + synchronized (mHdmiClientLock) { + if (mHdmiManager == null) return; + updateHdmiCecSinkLocked(isCecEnabled ? isCecAvailable : false); + } + } + }; + private final Object mHdmiClientLock = new Object(); // If HDMI-CEC system audio is supported @@ -5815,12 +5823,14 @@ public class AudioService extends IAudioService.Stub @GuardedBy("mHdmiClientLock") private HdmiPlaybackClient mHdmiPlaybackClient; // true if we are a set-top box, an HDMI sink is connected and it supports CEC. + @GuardedBy("mHdmiClientLock") private boolean mHdmiCecSink; // Set only when device is an audio system. @GuardedBy("mHdmiClientLock") private HdmiAudioSystemClient mHdmiAudioSystemClient; - private MyDisplayStatusCallback mHdmiDisplayStatusCallback = new MyDisplayStatusCallback(); + private MyHdmiControlStatusChangeListenerCallback mHdmiControlStatusChangeListenerCallback = + new MyHdmiControlStatusChangeListenerCallback(); @Override public int setHdmiSystemAudioSupported(boolean on) { diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 3398d36ffdb9..a83dd215593f 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -42,6 +42,7 @@ import android.hardware.hdmi.HdmiHotplugEvent; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.hdmi.IHdmiControlService; +import android.hardware.hdmi.IHdmiControlStatusChangeListener; import android.hardware.hdmi.IHdmiDeviceEventListener; import android.hardware.hdmi.IHdmiHotplugEventListener; import android.hardware.hdmi.IHdmiInputChangeListener; @@ -251,6 +252,11 @@ public class HdmiControlService extends SystemService { // Type of logical devices hosted in the system. Stored in the unmodifiable list. private final List<Integer> mLocalDevices; + // List of records for HDMI control status change listener for death monitoring. + @GuardedBy("mLock") + private final ArrayList<HdmiControlStatusChangeListenerRecord> + mHdmiControlStatusChangeListenerRecords = new ArrayList<>(); + // List of records for hotplug event listener to handle the the caller killed in action. @GuardedBy("mLock") private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = @@ -564,6 +570,7 @@ public class HdmiControlService extends SystemService { } if (reason != -1) { invokeVendorCommandListenersOnControlStateChanged(true, reason); + announceHdmiControlStatusChange(true); } } @@ -1317,6 +1324,37 @@ public class HdmiControlService extends SystemService { // Record class that monitors the event of the caller of being killed. Used to clean up // the listener list and record list accordingly. + private final class HdmiControlStatusChangeListenerRecord implements IBinder.DeathRecipient { + private final IHdmiControlStatusChangeListener mListener; + + HdmiControlStatusChangeListenerRecord(IHdmiControlStatusChangeListener listener) { + mListener = listener; + } + + @Override + public void binderDied() { + synchronized (mLock) { + mHdmiControlStatusChangeListenerRecords.remove(this); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HdmiControlStatusChangeListenerRecord)) return false; + if (obj == this) return true; + HdmiControlStatusChangeListenerRecord other = + (HdmiControlStatusChangeListenerRecord) obj; + return other.mListener == this.mListener; + } + + @Override + public int hashCode() { + return mListener.hashCode(); + } + } + + // Record class that monitors the event of the caller of being killed. Used to clean up + // the listener list and record list accordingly. private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { private final IHdmiHotplugEventListener mListener; @@ -1621,6 +1659,20 @@ public class HdmiControlService extends SystemService { } @Override + public void addHdmiControlStatusChangeListener( + final IHdmiControlStatusChangeListener listener) { + enforceAccessPermission(); + HdmiControlService.this.addHdmiControlStatusChangeListener(listener); + } + + @Override + public void removeHdmiControlStatusChangeListener( + final IHdmiControlStatusChangeListener listener) { + enforceAccessPermission(); + HdmiControlService.this.removeHdmiControlStatusChangeListener(listener); + } + + @Override public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { enforceAccessPermission(); HdmiControlService.this.addHotplugEventListener(listener); @@ -2135,6 +2187,51 @@ public class HdmiControlService extends SystemService { source.queryDisplayStatus(callback); } + private void addHdmiControlStatusChangeListener( + final IHdmiControlStatusChangeListener listener) { + final HdmiControlStatusChangeListenerRecord record = + new HdmiControlStatusChangeListenerRecord(listener); + try { + listener.asBinder().linkToDeath(record, 0); + } catch (RemoteException e) { + Slog.w(TAG, "Listener already died"); + return; + } + synchronized (mLock) { + mHdmiControlStatusChangeListenerRecords.add(record); + } + + // Inform the listener of the initial state of each HDMI port by generating + // hotplug events. + runOnServiceThread(new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (!mHdmiControlStatusChangeListenerRecords.contains(record)) return; + } + + // Return the current status of mHdmiControlEnabled; + synchronized (mLock) { + invokeHdmiControlStatusChangeListenerLocked(listener, mHdmiControlEnabled); + } + } + }); + } + + private void removeHdmiControlStatusChangeListener( + final IHdmiControlStatusChangeListener listener) { + synchronized (mLock) { + for (HdmiControlStatusChangeListenerRecord record : + mHdmiControlStatusChangeListenerRecords) { + if (record.mListener.asBinder() == listener.asBinder()) { + listener.asBinder().unlinkToDeath(record, 0); + mHdmiControlStatusChangeListenerRecords.remove(record); + break; + } + } + } + } + private void addHotplugEventListener(final IHdmiHotplugEventListener listener) { final HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); try { @@ -2367,6 +2464,47 @@ public class HdmiControlService extends SystemService { } } + private void announceHdmiControlStatusChange(boolean isEnabled) { + assertRunOnServiceThread(); + synchronized (mLock) { + for (HdmiControlStatusChangeListenerRecord record : + mHdmiControlStatusChangeListenerRecords) { + invokeHdmiControlStatusChangeListenerLocked(record.mListener, isEnabled); + } + } + } + + private void invokeHdmiControlStatusChangeListenerLocked( + IHdmiControlStatusChangeListener listener, boolean isEnabled) { + if (isEnabled) { + queryDisplayStatus(new IHdmiControlCallback.Stub() { + public void onComplete(int status) { + boolean isAvailable = true; + if (status == HdmiControlManager.POWER_STATUS_UNKNOWN + || status == HdmiControlManager.RESULT_EXCEPTION + || status == HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE) { + isAvailable = false; + } + + try { + listener.onStatusChange(isEnabled, isAvailable); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report HdmiControlStatusChange: " + isEnabled + + " isAvailable: " + isAvailable, e); + } + } + }); + return; + } + + try { + listener.onStatusChange(isEnabled, false); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report HdmiControlStatusChange: " + isEnabled + + " isAvailable: " + false, e); + } + } + public HdmiCecLocalDeviceTv tv() { return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiDeviceInfo.DEVICE_TV); } @@ -2736,6 +2874,8 @@ public class HdmiControlService extends SystemService { disableHdmiControlService(); } }); + announceHdmiControlStatusChange(enabled); + return; } |