From b36d4a13a07773256e6cd244e1d5268dc26d4d17 Mon Sep 17 00:00:00 2001 From: Eric Laurent Date: Fri, 9 Oct 2020 09:52:49 -0700 Subject: [PATCH] AudioManager: Add communication device management APIs Add new APIs to manage the audio device used for communication use cases (Cellular calls, VoIP and Video calls). These APIs are meant to replace specific APIs like setSpeakerPhoneOn() and provide a single generic way of configuring the audio device selected for calls. They will be used for newly added device types (e.g. BLE audio) instead of adding new specific APIs. They will also offer more options like for instance allowing to select the earpiece (handset) device when a wired headset is connected. The new APIs are: - boolean setDeviceForCommunication(AudioDeviceInfo) - void clearDeviceForCommunication() - AudioDeviceInfo getDeviceForCommunication() A listener is also added for applications to monitor current communication device selection: - OnCommunicationDeviceChangedListener As well as listener registration and unregistration APIs: - addOnCommunicationDeviceChangedListener() - removeOnCommunicationDeviceChangedListener() Bug: 161358428 Test: make && atest AudioCommunicationDeviceTest Change-Id: I8028d842e4a8ca1abe0f87d03e3c5d57c46b9362 --- core/api/current.txt | 9 + core/api/test-current.txt | 1 + media/java/android/media/AudioManager.java | 314 ++++++++++++++++++ media/java/android/media/IAudioService.aidl | 10 + .../media/ICommunicationDeviceDispatcher.aidl | 28 ++ .../server/audio/AudioDeviceBroker.java | 94 +++++- .../android/server/audio/AudioService.java | 110 ++++++ 7 files changed, 561 insertions(+), 5 deletions(-) create mode 100644 media/java/android/media/ICommunicationDeviceDispatcher.aidl diff --git a/core/api/current.txt b/core/api/current.txt index c2f88d7161c74..9624eaf403de1 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -19670,15 +19670,18 @@ package android.media { public class AudioManager { method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener); method public int abandonAudioFocusRequest(@NonNull android.media.AudioFocusRequest); + method public void addOnCommunicationDeviceChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnCommunicationDeviceChangedListener); method public void adjustStreamVolume(int, int, int); method public void adjustSuggestedStreamVolume(int, int, int); method public void adjustVolume(int, int); + method public void clearDeviceForCommunication(); method public void dispatchMediaKeyEvent(android.view.KeyEvent); method public int generateAudioSessionId(); method @NonNull public java.util.List getActivePlaybackConfigurations(); method @NonNull public java.util.List getActiveRecordingConfigurations(); method public int getAllowedCapturePolicy(); method public int getAudioHwSyncForSession(int); + method @Nullable public android.media.AudioDeviceInfo getDeviceForCommunication(); method public android.media.AudioDeviceInfo[] getDevices(int); method public java.util.List getMicrophones() throws java.io.IOException; method public int getMode(); @@ -19714,11 +19717,13 @@ package android.media { method @Deprecated public void registerMediaButtonEventReceiver(android.app.PendingIntent); method @Deprecated public void registerRemoteControlClient(android.media.RemoteControlClient); method @Deprecated public boolean registerRemoteController(android.media.RemoteController); + method public void removeOnCommunicationDeviceChangedListener(@NonNull android.media.AudioManager.OnCommunicationDeviceChangedListener); method @Deprecated public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, int, int); method public int requestAudioFocus(@NonNull android.media.AudioFocusRequest); method public void setAllowedCapturePolicy(int); method @Deprecated public void setBluetoothA2dpOn(boolean); method public void setBluetoothScoOn(boolean); + method public boolean setDeviceForCommunication(@NonNull android.media.AudioDeviceInfo); method public void setMicrophoneMute(boolean); method public void setMode(int); method public void setParameters(String); @@ -19856,6 +19861,10 @@ package android.media { method public void onAudioFocusChange(int); } + public static interface AudioManager.OnCommunicationDeviceChangedListener { + method public void onCommunicationDeviceChanged(@Nullable android.media.AudioDeviceInfo); + } + public final class AudioMetadata { method @NonNull public static android.media.AudioMetadataMap createMap(); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index f51bb95bd710b..e76b30d71728e 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -985,6 +985,7 @@ package android.media { } public class AudioManager { + method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int); method public boolean hasRegisteredDynamicPolicy(); } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index d9cfb638d0689..367b78470cd53 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -6182,6 +6182,29 @@ public class AudioManager { return infoListFromPortList(ports, flags); } + /** + * Returns an {@link AudioDeviceInfo} corresponding to the specified {@link AudioPort} ID. + * @param portId The audio port ID to look up for. + * @param flags A set of bitflags specifying the criteria to test. + * @see #GET_DEVICES_OUTPUTS + * @see #GET_DEVICES_INPUTS + * @see #GET_DEVICES_ALL + * @return An AudioDeviceInfo or null if no device with matching port ID is found. + * @hide + */ + public static AudioDeviceInfo getDeviceForPortId(int portId, int flags) { + if (portId == 0) { + return null; + } + AudioDeviceInfo[] devices = getDevicesStatic(flags); + for (AudioDeviceInfo device : devices) { + if (device.getId() == portId) { + return device; + } + } + return null; + } + /** * Registers an {@link AudioDeviceCallback} object to receive notifications of changes * to the set of connected audio devices. @@ -6869,6 +6892,297 @@ public class AudioManager { return hwSyncId; } + /** + * Selects the audio device that should be used for communication use cases, for instance voice + * or video calls. This method can be used by voice or video chat applications to select a + * different audio device than the one selected by default by the platform. + *

The device selection is expressed as an {@link AudioDeviceInfo}, of role sink + * ({@link AudioDeviceInfo#isSink()} is true) and of one of the following types: + *

    + *
  • {@link AudioDeviceInfo#TYPE_BUILTIN_EARPIECE} + *
  • {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER} + *
  • {@link AudioDeviceInfo#TYPE_WIRED_HEADSET} + *
  • {@link AudioDeviceInfo#TYPE_BLUETOOTH_SCO} + *
  • {@link AudioDeviceInfo#TYPE_USB_HEADSET} + *
  • {@link AudioDeviceInfo#TYPE_BLE_HEADSET} + *
+ * The selection is active as long as the requesting application lives, until + * {@link #clearDeviceForCommunication} is called or until the device is disconnected. + * It is therefore important for applications to clear the request when a call ends or the + * application is paused. + *

In case of simultaneous requests by multiple applications the priority is given to the + * application currently controlling the audio mode (see {@link #setMode(int)}). This is the + * latest application having selected mode {@link #MODE_IN_COMMUNICATION} or mode + * {@link #MODE_IN_CALL}. Note that MODE_IN_CALL can only be selected by the main + * telephony application with permission + * {@link android.Manifest.permission#MODIFY_PHONE_STATE}. + *

If the requested devices is not currently available, the request will be rejected and + * the method will return false. + *

This API replaces the following deprecated APIs: + *

    + *
  • {@link #startBluetoothSco()} + *
  • {@link #stopBluetoothSco()} + *
  • {@link #setSpeakerphoneOn(boolean)} + *
+ *

Example

+ *

The example below shows how to enable and disable speakerphone mode. + *

+     * // Get an AudioManager instance
+     * AudioManager audioManager = Context.getSystemService(AudioManager.class);
+     * try {
+     *     AudioDeviceInfo speakerDevice = null;
+     *     AudioDeviceInfo[] devices = audioManager.getDevices(GET_DEVICES_OUTPUTS);
+     *     for (AudioDeviceInfo device : devices) {
+     *         if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+     *             speakerDevice = device;
+     *             break;
+     *         }
+     *     }
+     *     if (speakerDevice != null) {
+     *         // Turn speakerphone ON.
+     *         boolean result = audioManager.setDeviceForCommunication(speakerDevice);
+     *         if (!result) {
+     *             // Handle error.
+     *         }
+     *         // Turn speakerphone OFF.
+     *         audioManager.clearDeviceForCommunication();
+     *     }
+     * } catch (IllegalArgumentException e) {
+     *     // Handle exception.
+     * }
+     * 
+ * @param device the requested audio device. + * @return true if the request was accepted, false otherwise. + * @throws IllegalArgumentException If an invalid device is specified. + */ + public boolean setDeviceForCommunication(@NonNull AudioDeviceInfo device) { + Objects.requireNonNull(device); + try { + if (device.getId() == 0) { + throw new IllegalArgumentException("In valid device: " + device); + } + return getService().setDeviceForCommunication(mICallBack, device.getId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Cancels previous communication device selection made with + * {@link #setDeviceForCommunication(AudioDeviceInfo)}. + */ + public void clearDeviceForCommunication() { + try { + getService().setDeviceForCommunication(mICallBack, 0); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns currently selected audio device for communication. + *

This API replaces the following deprecated APIs: + *

    + *
  • {@link #isBluetoothScoOn()} + *
  • {@link #isSpeakerphoneOn()} + *
+ * @return an {@link AudioDeviceInfo} indicating which audio device is + * currently selected or communication use cases or null if default selection + * is used. + */ + @Nullable + public AudioDeviceInfo getDeviceForCommunication() { + try { + return getDeviceForPortId( + getService().getDeviceForCommunication(), GET_DEVICES_OUTPUTS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Returns an {@link AudioDeviceInfo} corresponding to a connected device of the type provided. + * The type must be a valid output type defined in AudioDeviceInfo class, + * for instance {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. + * The method will return null if no device of the provided type is connected. + * If more than one device of the provided type is connected, an object corresponding to the + * first device encountered in the enumeration list will be returned. + * @param deviceType The device device for which an AudioDeviceInfo + * object is queried. + * @return An AudioDeviceInfo object or null if no device with the requested type is connected. + * @throws IllegalArgumentException If an invalid device type is specified. + */ + @TestApi + @Nullable + public static AudioDeviceInfo getDeviceInfoFromType( + @AudioDeviceInfo.AudioDeviceTypeOut int deviceType) { + AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo device : devices) { + if (device.getType() == deviceType) { + return device; + } + } + return null; + } + + /** + * Listener registered by client to be notified upon communication audio device change. + * See {@link #setDeviceForCommunication(AudioDeviceInfo)}. + */ + public interface OnCommunicationDeviceChangedListener { + /** + * Callback method called upon communication audio device change. + * @param device the audio device selected for communication use cases + */ + void onCommunicationDeviceChanged(@Nullable AudioDeviceInfo device); + } + + /** + * Adds a listener for being notified of changes to the communication audio device. + * See {@link #setDeviceForCommunication(AudioDeviceInfo)}. + * @param executor + * @param listener + */ + public void addOnCommunicationDeviceChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnCommunicationDeviceChangedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mCommDevListenerLock) { + if (hasCommDevListener(listener)) { + throw new IllegalArgumentException( + "attempt to call addOnCommunicationDeviceChangedListener() " + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (mCommDevListeners == null) { + mCommDevListeners = new ArrayList<>(); + } + final int oldCbCount = mCommDevListeners.size(); + mCommDevListeners.add(new CommDevListenerInfo(listener, executor)); + if (oldCbCount == 0 && mCommDevListeners.size() > 0) { + // register binder for callbacks + if (mCommDevDispatcherStub == null) { + mCommDevDispatcherStub = new CommunicationDeviceDispatcherStub(); + } + try { + getService().registerCommunicationDeviceDispatcher(mCommDevDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * Removes a previously added listener of changes to the communication audio device. + * See {@link #setDeviceForCommunication(AudioDeviceInfo)}. + * @param listener + */ + public void removeOnCommunicationDeviceChangedListener( + @NonNull OnCommunicationDeviceChangedListener listener) { + Objects.requireNonNull(listener); + synchronized (mCommDevListenerLock) { + if (!removeCommDevListener(listener)) { + throw new IllegalArgumentException( + "attempt to call removeOnCommunicationDeviceChangedListener() " + + "on an unregistered listener"); + } + if (mCommDevListeners.size() == 0) { + // unregister binder for callbacks + try { + getService().unregisterCommunicationDeviceDispatcher( + mCommDevDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + mCommDevDispatcherStub = null; + mCommDevListeners = null; + } + } + } + } + + private final Object mCommDevListenerLock = new Object(); + /** + * List of listeners for preferred device for strategy and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mCommDevListenerLock") + private @Nullable ArrayList mCommDevListeners; + + private static class CommDevListenerInfo { + final @NonNull OnCommunicationDeviceChangedListener mListener; + final @NonNull Executor mExecutor; + + CommDevListenerInfo(OnCommunicationDeviceChangedListener listener, Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + @GuardedBy("mCommDevListenerLock") + private CommunicationDeviceDispatcherStub mCommDevDispatcherStub; + + private final class CommunicationDeviceDispatcherStub + extends ICommunicationDeviceDispatcher.Stub { + + @Override + public void dispatchCommunicationDeviceChanged(int portId) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList commDevListeners; + synchronized (mCommDevListenerLock) { + if (mCommDevListeners == null || mCommDevListeners.size() == 0) { + return; + } + commDevListeners = (ArrayList) mCommDevListeners.clone(); + } + AudioDeviceInfo device = getDeviceForPortId(portId, GET_DEVICES_OUTPUTS); + final long ident = Binder.clearCallingIdentity(); + try { + for (CommDevListenerInfo info : commDevListeners) { + info.mExecutor.execute(() -> + info.mListener.onCommunicationDeviceChanged(device)); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + @GuardedBy("mCommDevListenerLock") + private @Nullable CommDevListenerInfo getCommDevListenerInfo( + OnCommunicationDeviceChangedListener listener) { + if (mCommDevListeners == null) { + return null; + } + for (CommDevListenerInfo info : mCommDevListeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + @GuardedBy("mCommDevListenerLock") + private boolean hasCommDevListener(OnCommunicationDeviceChangedListener listener) { + return getCommDevListenerInfo(listener) != null; + } + + @GuardedBy("mCommDevListenerLock") + /** + * @return true if the listener was removed from the list + */ + private boolean removeCommDevListener(OnCommunicationDeviceChangedListener listener) { + final CommDevListenerInfo infoToRemove = getCommDevListenerInfo(listener); + if (infoToRemove != null) { + mCommDevListeners.remove(infoToRemove); + return true; + } + return false; + } + //--------------------------------------------------------- // Inner classes //-------------------- diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 77fde6a7d6255..b6bb3a3df38bd 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -28,6 +28,7 @@ import android.media.IAudioFocusDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; import android.media.ICapturePresetDevicesRoleDispatcher; +import android.media.ICommunicationDeviceDispatcher; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; @@ -338,4 +339,13 @@ interface IAudioService { boolean isMusicActive(in boolean remotely); int getDevicesForStream(in int streamType); + + boolean setDeviceForCommunication(IBinder cb, int portId); + + int getDeviceForCommunication(); + + void registerCommunicationDeviceDispatcher(ICommunicationDeviceDispatcher dispatcher); + + oneway void unregisterCommunicationDeviceDispatcher( + ICommunicationDeviceDispatcher dispatcher); } diff --git a/media/java/android/media/ICommunicationDeviceDispatcher.aidl b/media/java/android/media/ICommunicationDeviceDispatcher.aidl new file mode 100644 index 0000000000000..429f934a77dc1 --- /dev/null +++ b/media/java/android/media/ICommunicationDeviceDispatcher.aidl @@ -0,0 +1,28 @@ +/* + * 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.media; + +/** + * AIDL for AudioService to signal audio communication device updates. + * + * {@hide} + */ +oneway interface ICommunicationDeviceDispatcher { + + void dispatchCommunicationDeviceChanged(int portId); + +} diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 3b407f14297ea..f04c5eb030dba 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -31,6 +31,7 @@ import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; import android.media.ICapturePresetDevicesRoleDispatcher; +import android.media.ICommunicationDeviceDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.MediaMetrics; import android.os.Binder; @@ -39,6 +40,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; @@ -233,6 +235,38 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + /** + * Select device for use for communication use cases. + * @param cb Client binder for death detection + * @param pid Client pid + * @param device Device selected or null to unselect. + * @param eventSource for logging purposes + */ + /*package*/ boolean setDeviceForCommunication( + IBinder cb, int pid, AudioDeviceInfo device, String eventSource) { + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setDeviceForCommunication, device: " + device + ", pid: " + pid); + } + + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + AudioDeviceAttributes deviceAttr = null; + if (device != null) { + deviceAttr = new AudioDeviceAttributes(device); + } else { + CommunicationRouteClient client = getCommunicationRouteClientForPid(pid); + if (client == null) { + return false; + } + } + setCommunicationRouteForClient( + cb, pid, deviceAttr, BtHelper.SCO_MODE_UNDEFINED, eventSource); + } + } + return true; + } + @GuardedBy("mDeviceStateLock") /*package*/ void setCommunicationRouteForClient( IBinder cb, int pid, AudioDeviceAttributes device, @@ -278,6 +312,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } else { removeCommunicationRouteClient(cb, true); } + postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); } } else if (!isBtScoRequested && wasBtScoRequested) { mBtHelper.stopBluetoothSco(eventSource); @@ -324,6 +359,20 @@ import java.util.concurrent.atomic.AtomicBoolean; return device; } + /** + * Returns the device currently requested for communication use case. + * @return AudioDeviceInfo the requested device for communication. + */ + AudioDeviceInfo getDeviceForCommunication() { + synchronized (mDeviceStateLock) { + AudioDeviceAttributes device = requestedCommunicationDevice(); + if (device == null) { + return null; + } + return AudioManager.getDeviceInfoFromType(device.getType()); + } + } + /** * Helper method on top of requestedCommunicationDevice() indicating if * speakerphone ON is currently requested or not. @@ -566,11 +615,6 @@ import java.util.concurrent.atomic.AtomicBoolean; AudioDeviceAttributes device = new AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BLUETOOTH_SCO, ""); setCommunicationRouteForClient(cb, pid, device, scoAudioMode, eventSource); - if (!isBluetoothScoRequested()) { - Log.w(TAG, "startBluetoothScoForClient_Sync: rejected for pid: " - + pid + " mode owner pid: " + mModeOwnerPid); - postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - } } } } @@ -632,6 +676,45 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.unregisterCapturePresetDevicesRoleDispatcher(dispatcher); } + /*package*/ void registerCommunicationDeviceDispatcher( + @NonNull ICommunicationDeviceDispatcher dispatcher) { + mCommDevDispatchers.register(dispatcher); + } + + /*package*/ void unregisterCommunicationDeviceDispatcher( + @NonNull ICommunicationDeviceDispatcher dispatcher) { + mCommDevDispatchers.unregister(dispatcher); + } + + // Monitoring of communication device + final RemoteCallbackList mCommDevDispatchers = + new RemoteCallbackList(); + + // portId of the device currently selected for communication: avoids broadcasting changes + // when same communication route is applied + @GuardedBy("mDeviceStateLock") + int mCurCommunicationPortId = -1; + + @GuardedBy("mDeviceStateLock") + private void dispatchCommunicationDevice() { + AudioDeviceInfo device = getDeviceForCommunication(); + int portId = (getDeviceForCommunication() == null) ? 0 : device.getId(); + if (portId == mCurCommunicationPortId) { + return; + } + mCurCommunicationPortId = portId; + + final int nbDispatchers = mCommDevDispatchers.beginBroadcast(); + for (int i = 0; i < nbDispatchers; i++) { + try { + mCommDevDispatchers.getBroadcastItem(i) + .dispatchCommunicationDeviceChanged(portId); + } catch (RemoteException e) { + } + } + mCommDevDispatchers.finishBroadcast(); + } + //--------------------------------------------------------------------- // Communication with (to) AudioService //TODO check whether the AudioService methods are candidates to move here @@ -1571,6 +1654,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } mAudioService.postUpdateRingerModeServiceInt(); + dispatchCommunicationDevice(); } private CommunicationRouteClient removeCommunicationRouteClient( diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a04184cee67a2..1db8ceb286415 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -85,6 +85,7 @@ import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; import android.media.IAudioService; import android.media.ICapturePresetDevicesRoleDispatcher; +import android.media.ICommunicationDeviceDispatcher; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; @@ -4417,6 +4418,115 @@ public class AudioService extends IAudioService.Stub restoreDeviceVolumeBehavior(); } + private static final int[] VALID_COMMUNICATION_DEVICE_TYPES = { + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioDeviceInfo.TYPE_USB_HEADSET, + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + AudioDeviceInfo.TYPE_HEARING_AID, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_USB_DEVICE, + AudioDeviceInfo.TYPE_BLE_SPEAKER, + AudioDeviceInfo.TYPE_LINE_ANALOG, + AudioDeviceInfo.TYPE_HDMI, + AudioDeviceInfo.TYPE_AUX_LINE + }; + + private boolean isValidCommunicationDevice(AudioDeviceInfo device) { + for (int type : VALID_COMMUNICATION_DEVICE_TYPES) { + if (device.getType() == type) { + return true; + } + } + return false; + } + + /** @see AudioManager#setDeviceForCommunication(int) */ + public boolean setDeviceForCommunication(IBinder cb, int portId) { + final int uid = Binder.getCallingUid(); + final int pid = Binder.getCallingPid(); + + AudioDeviceInfo device = null; + if (portId != 0) { + device = AudioManager.getDeviceForPortId(portId, AudioManager.GET_DEVICES_OUTPUTS); + if (device == null) { + throw new IllegalArgumentException("invalid portID " + portId); + } + if (!isValidCommunicationDevice(device)) { + throw new IllegalArgumentException("invalid device type " + device.getType()); + } + } + final String eventSource = new StringBuilder("setDeviceForCommunication(") + .append(") from u/pid:").append(uid).append("/") + .append(pid).toString(); + + int deviceType = AudioSystem.DEVICE_OUT_DEFAULT; + String deviceAddress = null; + if (device != null) { + deviceType = device.getPort().type(); + deviceAddress = device.getAddress(); + } else { + AudioDeviceInfo curDevice = mDeviceBroker.getDeviceForCommunication(); + if (curDevice != null) { + deviceType = curDevice.getPort().type(); + deviceAddress = curDevice.getAddress(); + } + } + // do not log metrics if clearing communication device while no communication device + // was selected + if (deviceType != AudioSystem.DEVICE_OUT_DEFAULT) { + new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + + MediaMetrics.SEPARATOR + "setDeviceForCommunication") + .set(MediaMetrics.Property.DEVICE, + AudioSystem.getDeviceName(deviceType)) + .set(MediaMetrics.Property.ADDRESS, deviceAddress) + .set(MediaMetrics.Property.STATE, device != null + ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED) + .record(); + } + + final long ident = Binder.clearCallingIdentity(); + boolean status = + mDeviceBroker.setDeviceForCommunication(cb, pid, device, eventSource); + Binder.restoreCallingIdentity(ident); + return status; + } + + /** @see AudioManager#getDeviceForCommunication() */ + public int getDeviceForCommunication() { + final long ident = Binder.clearCallingIdentity(); + AudioDeviceInfo device = mDeviceBroker.getDeviceForCommunication(); + Binder.restoreCallingIdentity(ident); + if (device == null) { + return 0; + } + return device.getId(); + } + + /** @see AudioManager#addOnCommunicationDeviceChangedListener( + * Executor, AudioManager.OnCommunicationDeviceChangedListener) + */ + public void registerCommunicationDeviceDispatcher( + @Nullable ICommunicationDeviceDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + mDeviceBroker.registerCommunicationDeviceDispatcher(dispatcher); + } + + /** @see AudioManager#removeOnCommunicationDeviceChangedListener( + * AudioManager.OnCommunicationDeviceChangedListener) + */ + public void unregisterCommunicationDeviceDispatcher( + @Nullable ICommunicationDeviceDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + mDeviceBroker.unregisterCommunicationDeviceDispatcher(dispatcher); + } + /** @see AudioManager#setSpeakerphoneOn(boolean) */ public void setSpeakerphoneOn(IBinder cb, boolean on) { if (!checkAudioSettingsPermission("setSpeakerphoneOn()")) {