From 933bf14319d68d52e646130b5ca31afeb1091f70 Mon Sep 17 00:00:00 2001 From: Jean-Michel Trivi Date: Fri, 19 Nov 2021 16:18:52 -0800 Subject: [PATCH 1/2] AudioManager: add API for muting playback until device connects Introduce API to implement the feature where playback is muted until a specific device connects. The muting call is accompanied by a timeout after which muted players are unmuted. Implementation: - AudioManager: API, management of events sent by AudioService - AudioService: tracks presence/absence of audio device that drives the muting, permission check - PlaybackActivityMonitor: mutes/unmutes existing players, mutes new players when created, keeps track of timeout. Bug: 200218901 Test: atest com.google.android.gts.audioservice.AudioServiceHostTest#testMuteAwaitConnection Change-Id: I014c542e5b74402a41081326a53ec9d7552db021 --- core/api/system-current.txt | 14 ++ media/java/android/media/AudioManager.java | 226 ++++++++++++++++++ media/java/android/media/IAudioService.aidl | 11 + .../media/IMuteAwaitConnectionCallback.aidl | 31 +++ .../server/audio/AudioDeviceBroker.java | 6 + .../server/audio/AudioDeviceInventory.java | 18 +- .../android/server/audio/AudioService.java | 191 ++++++++++++++- .../server/audio/PlaybackActivityMonitor.java | 187 ++++++++++++++- 8 files changed, 671 insertions(+), 13 deletions(-) create mode 100644 media/java/android/media/IMuteAwaitConnectionCallback.aidl diff --git a/core/api/system-current.txt b/core/api/system-current.txt index a298354af1b36..992789bbbb75f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -5403,6 +5403,7 @@ package android.media { method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener) throws java.lang.SecurityException; + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void cancelMuteAwaitConnection(@NonNull android.media.AudioDeviceAttributes) throws java.lang.IllegalStateException; method public void clearAudioServerStateCallback(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean clearPreferredDevicesForCapturePreset(int); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int dispatchAudioFocusChange(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy); @@ -5416,6 +5417,7 @@ package android.media { method @IntRange(from=0) public long getMaxAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo); method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMaxVolumeIndexForAttributes(@NonNull android.media.AudioAttributes); method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMinVolumeIndexForAttributes(@NonNull android.media.AudioAttributes); + method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getMutingExpectedDevice(); method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getPreferredDeviceForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List getPreferredDevicesForCapturePreset(int); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List getPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy); @@ -5424,7 +5426,9 @@ package android.media { method public boolean isAudioServerRunning(); method public boolean isHdmiSystemAudioSupported(); method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable(); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback); method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener); @@ -5444,6 +5448,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setVolumeIndexForAttributes(@NonNull android.media.AudioAttributes, int, int); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback); method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback); field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1 field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4 @@ -5463,6 +5468,15 @@ package android.media { method public void onAudioServerUp(); } + public abstract static class AudioManager.MuteAwaitConnectionCallback { + ctor public AudioManager.MuteAwaitConnectionCallback(); + method public void onMutedUntilConnection(@NonNull android.media.AudioDeviceAttributes, @NonNull int[]); + method public void onUnmutedEvent(int, @NonNull android.media.AudioDeviceAttributes, @NonNull int[]); + field public static final int EVENT_CANCEL = 3; // 0x3 + field public static final int EVENT_CONNECTION = 1; // 0x1 + field public static final int EVENT_TIMEOUT = 2; // 0x2 + } + @Deprecated public static interface AudioManager.OnPreferredDeviceForStrategyChangedListener { method @Deprecated public void onPreferredDeviceForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @Nullable android.media.AudioDeviceAttributes); } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 46aad3f11ac8d..9bde8c3e59ccb 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -89,6 +89,7 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** * AudioManager provides access to volume and ringer mode control. @@ -7967,6 +7968,231 @@ public class AudioManager { } } + //--------------------------------------------------------- + // audio device connection-dependent muting + /** + * @hide + * Mute a set of playback use cases until a given audio device is connected. + * Automatically unmute upon connection of the device, or after the given timeout, whichever + * happens first. + * @param usagesToMute non-empty array of {@link AudioAttributes} usages (for example + * {@link AudioAttributes#USAGE_MEDIA}) to mute until the + * device connects + * @param device the audio device expected to connect within the timeout duration + * @param timeout the maximum amount of time to wait for the device connection + * @param timeUnit the unit for the timeout + * @throws IllegalStateException when trying to issue the command while another is already in + * progress and hasn't been cancelled by + * {@link #cancelMuteAwaitConnection(AudioDeviceAttributes)}. See + * {@link #getMutingExpectedDevice()} to check if a muting command is active. + * @see #registerMuteAwaitConnectionCallback(Executor, AudioManager.MuteAwaitConnectionCallback) + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void muteAwaitConnection(@NonNull int[] usagesToMute, + @NonNull AudioDeviceAttributes device, + long timeout, @NonNull TimeUnit timeUnit) throws IllegalStateException { + if (timeout <= 0) { + throw new IllegalArgumentException("Timeout must be greater than 0"); + } + Objects.requireNonNull(usagesToMute); + if (usagesToMute.length == 0) { + throw new IllegalArgumentException("Array of usages to mute cannot be empty"); + } + Objects.requireNonNull(device); + Objects.requireNonNull(timeUnit); + try { + getService().muteAwaitConnection(usagesToMute, device, timeUnit.toMillis(timeout)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Query which audio device, if any, is causing some playback use cases to be muted until it + * connects. + * @return the audio device used in + * {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)}, or null + * if there is no active muting command (either because the muting command was not issued + * or because it timed out) + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public @Nullable AudioDeviceAttributes getMutingExpectedDevice() { + try { + return getService().getMutingExpectedDevice(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Cancel a {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)} + * command. + * @param device the device whose connection was expected when the {@code muteAwaitConnection} + * command was issued. + * @throws IllegalStateException when trying to issue the command for a device whose connection + * is not anticipated by a previous call to + * {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)} + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void cancelMuteAwaitConnection(@NonNull AudioDeviceAttributes device) + throws IllegalStateException { + Objects.requireNonNull(device); + try { + getService().cancelMuteAwaitConnection(device); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * A callback class to receive events about the muting and unmuting of playback use cases + * conditional on the upcoming connection of an audio device. + * @see #registerMuteAwaitConnectionCallback(Executor, AudioManager.MuteAwaitConnectionCallback) + */ + @SystemApi + public abstract static class MuteAwaitConnectionCallback { + + /** + * An event where the expected audio device connected + * @see MuteAwaitConnectionCallback#onUnmutedEvent(int, AudioDeviceAttributes, int[]) + */ + public static final int EVENT_CONNECTION = 1; + /** + * An event where the expected audio device failed connect before the timeout happened + * @see MuteAwaitConnectionCallback#onUnmutedEvent(int, AudioDeviceAttributes, int[]) + */ + public static final int EVENT_TIMEOUT = 2; + /** + * An event where the {@code muteAwaitConnection()} command + * was cancelled with {@link #cancelMuteAwaitConnection(AudioDeviceAttributes)} + * @see MuteAwaitConnectionCallback#onUnmutedEvent(int, AudioDeviceAttributes, int[]) + */ + public static final int EVENT_CANCEL = 3; + + /** @hide */ + @IntDef(flag = false, prefix = "EVENT_", value = { + EVENT_CONNECTION, + EVENT_TIMEOUT, + EVENT_CANCEL } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface UnmuteEvent {} + + /** + * Called when a number of playback use cases are muted in response to a call to + * {@link #muteAwaitConnection(int[], AudioDeviceAttributes, long, TimeUnit)}. + * @param device the audio device whose connection is expected. Playback use cases are + * unmuted when that device connects + * @param mutedUsages an array of {@link AudioAttributes} usages that describe the affected + * playback use cases. + */ + public void onMutedUntilConnection( + @NonNull AudioDeviceAttributes device, + @NonNull int[] mutedUsages) {} + + /** + * Called when an event occurred that caused playback uses cases to be unmuted + * @param unmuteEvent the nature of the event + * @param device the device that was expected to connect + * @param mutedUsages the array of {@link AudioAttributes} usages that were muted until + * the event occurred + */ + public void onUnmutedEvent( + @UnmuteEvent int unmuteEvent, + @NonNull AudioDeviceAttributes device, @NonNull int[] mutedUsages) {} + } + + + /** + * @hide + * Register a callback to receive updates on the playback muting conditional on a specific + * audio device connection. + * @param executor the {@link Executor} handling the callback + * @param callback the callback to register + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void registerMuteAwaitConnectionCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull MuteAwaitConnectionCallback callback) { + synchronized (mMuteAwaitConnectionListenerLock) { + final Pair>, + MuteAwaitConnectionDispatcherStub> res = + CallbackUtil.addListener("registerMuteAwaitConnectionCallback", + executor, callback, mMuteAwaitConnectionListeners, + mMuteAwaitConnDispatcherStub, + () -> new MuteAwaitConnectionDispatcherStub(), + stub -> stub.register(true)); + mMuteAwaitConnectionListeners = res.first; + mMuteAwaitConnDispatcherStub = res.second; + } + } + + /** + * @hide + * Unregister a previously registered callback for playback muting conditional on device + * connection. + * @param callback the callback to unregister + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void unregisterMuteAwaitConnectionCallback( + @NonNull MuteAwaitConnectionCallback callback) { + synchronized (mMuteAwaitConnectionListenerLock) { + final Pair>, + MuteAwaitConnectionDispatcherStub> res = + CallbackUtil.removeListener("unregisterMuteAwaitConnectionCallback", + callback, mMuteAwaitConnectionListeners, mMuteAwaitConnDispatcherStub, + stub -> stub.register(false)); + mMuteAwaitConnectionListeners = res.first; + mMuteAwaitConnDispatcherStub = res.second; + } + } + + private final Object mMuteAwaitConnectionListenerLock = new Object(); + + @GuardedBy("mMuteAwaitConnectionListenerLock") + private @Nullable ArrayList> + mMuteAwaitConnectionListeners; + + @GuardedBy("mMuteAwaitConnectionListenerLock") + private MuteAwaitConnectionDispatcherStub mMuteAwaitConnDispatcherStub; + + private final class MuteAwaitConnectionDispatcherStub + extends IMuteAwaitConnectionCallback.Stub { + public void register(boolean register) { + try { + getService().registerMuteAwaitConnectionDispatcher(this, register); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method + public void dispatchOnMutedUntilConnection(AudioDeviceAttributes device, + int[] mutedUsages) { + CallbackUtil.callListeners(mMuteAwaitConnectionListeners, + mMuteAwaitConnectionListenerLock, + (listener) -> listener.onMutedUntilConnection(device, mutedUsages)); + } + + @Override + @SuppressLint("GuardedBy") // lock applied inside callListeners method + public void dispatchOnUnmutedEvent(int event, AudioDeviceAttributes device, + int[] mutedUsages) { + CallbackUtil.callListeners(mMuteAwaitConnectionListeners, + mMuteAwaitConnectionListenerLock, + (listener) -> listener.onUnmutedEvent(event, device, mutedUsages)); + } + } + //--------------------------------------------------------- // Inner classes //-------------------- diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index f15f880a42a70..67f366473134e 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -32,6 +32,7 @@ import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; import android.media.ICapturePresetDevicesRoleDispatcher; import android.media.ICommunicationDeviceDispatcher; +import android.media.IMuteAwaitConnectionCallback; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; @@ -447,4 +448,14 @@ interface IAudioService { boolean isVolumeFixed(); boolean isPstnCallAudioInterceptable(); + + oneway void muteAwaitConnection(in int[] usagesToMute, in AudioDeviceAttributes dev, + long timeOutMs); + + oneway void cancelMuteAwaitConnection(in AudioDeviceAttributes dev); + + AudioDeviceAttributes getMutingExpectedDevice(); + + void registerMuteAwaitConnectionDispatcher(in IMuteAwaitConnectionCallback cb, + boolean register); } diff --git a/media/java/android/media/IMuteAwaitConnectionCallback.aidl b/media/java/android/media/IMuteAwaitConnectionCallback.aidl new file mode 100644 index 0000000000000..77fc02960d5f3 --- /dev/null +++ b/media/java/android/media/IMuteAwaitConnectionCallback.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 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; + +import android.media.AudioDeviceAttributes; + +/** + * AIDL for the AudioService to signal mute events tied to audio device connections. + * + * {@hide} + */ +oneway interface IMuteAwaitConnectionCallback { + + void dispatchOnMutedUntilConnection(in AudioDeviceAttributes device, in int[] mutedUsages); + + void dispatchOnUnmutedEvent(int event, in AudioDeviceAttributes device, in int[] mutedUsages); +} diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index ff451a30dd695..3bd723e1187cc 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -493,6 +493,12 @@ import java.util.concurrent.atomic.AtomicBoolean; return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLUETOOTH_SCO); } + /*package*/ boolean isDeviceConnected(@NonNull AudioDeviceAttributes device) { + synchronized (mDeviceStateLock) { + return mDeviceInventory.isDeviceConnected(device); + } + } + /*package*/ void setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state, String address, String name, String caller) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index f32d3b52d61cc..fdb2970f6339f 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -593,7 +593,7 @@ public class AudioDeviceInventory { } //------------------------------------------------------------ - // + // preferred device(s) /*package*/ int setPreferredDevicesForStrategySync(int strategy, @NonNull List devices) { @@ -674,6 +674,22 @@ public class AudioDeviceInventory { mDevRoleCapturePresetDispatchers.unregister(dispatcher); } + //----------------------------------------------------------------------- + + /** + * Check if a device is in the list of connected devices + * @param device the device whose connection state is queried + * @return true if connected + */ + @GuardedBy("AudioDeviceBroker.mDeviceStateLock") + public boolean isDeviceConnected(@NonNull AudioDeviceAttributes device) { + final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(), + device.getAddress()); + synchronized (mDevicesLock) { + return (mConnectedDevices.get(key) != null); + } + } + /** * Implements the communication with AudioSystem to (dis)connect a device in the native layers * @param connect true if connection diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 9c8a663545b75..3eeffcb82e27a 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -35,6 +35,7 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; @@ -92,6 +93,7 @@ import android.media.IAudioServerStateDispatcher; import android.media.IAudioService; import android.media.ICapturePresetDevicesRoleDispatcher; import android.media.ICommunicationDeviceDispatcher; +import android.media.IMuteAwaitConnectionCallback; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; @@ -1027,7 +1029,8 @@ public class AudioService extends IAudioService.Stub readUserRestrictions(); mPlaybackMonitor = - new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM]); + new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM], + device -> onMuteAwaitConnectionTimeout(device)); mPlaybackMonitor.registerPlaybackCallback(mVoicePlaybackActivityMonitor, true); mMediaFocusControl = new MediaFocusControl(mContext, mPlaybackMonitor); @@ -1048,6 +1051,9 @@ public class AudioService extends IAudioService.Stub mHasSpatializerEffect = SystemProperties.getBoolean("ro.audio.spatializer_enabled", false); + // monitor routing updates coming from native + mAudioSystem.setRoutingListener(this); + // done with service initialization, continue additional work in our Handler thread queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_STREAMS_VOLUMES, 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */); @@ -1251,20 +1257,22 @@ public class AudioService extends IAudioService.Stub // routing monitoring from AudioSystemAdapter @Override public void onRoutingUpdatedFromNative() { - if (!mHasSpatializerEffect) { - return; - } sendMsg(mAudioHandler, MSG_ROUTING_UPDATED, SENDMSG_REPLACE, 0, 0, null, /*delay*/ 0); } - void monitorRoutingChanges(boolean enabled) { - mAudioSystem.setRoutingListener(enabled ? this : null); + /** + * called when handling MSG_ROUTING_UPDATED + */ + void onRoutingUpdatedFromAudioThread() { + if (mHasSpatializerEffect) { + mSpatializerHelper.onRoutingUpdated(); + } + checkMuteAwaitConnection(); } - //----------------------------------------------------------------- RoleObserver mRoleObserver; @@ -1452,7 +1460,6 @@ public class AudioService extends IAudioService.Stub if (mHasSpatializerEffect) { mSpatializerHelper.reset(/* featureEnabled */ isSpatialAudioEnabled()); - monitorRoutingChanges(true); } onIndicateSystemReady(); @@ -7652,7 +7659,6 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect); if (mHasSpatializerEffect) { mSpatializerHelper.setFeatureEnabled(isSpatialAudioEnabled()); - monitorRoutingChanges(true); } mAudioEventWakeLock.release(); break; @@ -7791,7 +7797,7 @@ public class AudioService extends IAudioService.Stub break; case MSG_ROUTING_UPDATED: - mSpatializerHelper.onRoutingUpdated(); + onRoutingUpdatedFromAudioThread(); break; case MSG_PERSIST_SPATIAL_AUDIO_ENABLED: @@ -8611,6 +8617,171 @@ public class AudioService extends IAudioService.Stub com.android.internal.R.bool.config_camera_sound_forced); } + //========================================================================================== + private final Object mMuteAwaitConnectionLock = new Object(); + + /** + * The device that is expected to be connected soon, and causes players to be muted until + * its connection, or it times out. + * Null when no active muting command, or it has timed out. + */ + @GuardedBy("mMuteAwaitConnectionLock") + private AudioDeviceAttributes mMutingExpectedDevice; + @GuardedBy("mMuteAwaitConnectionLock") + private @Nullable int[] mMutedUsagesAwaitingConnection; + + /** @see AudioManager#muteAwaitConnection */ + @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection + public void muteAwaitConnection(@NonNull int[] usages, + @NonNull AudioDeviceAttributes device, long timeOutMs) { + Objects.requireNonNull(usages); + Objects.requireNonNull(device); + enforceModifyAudioRoutingPermission(); + if (timeOutMs <= 0 || usages.length == 0) { + throw new IllegalArgumentException("Invalid timeOutMs/usagesToMute"); + } + + if (mDeviceBroker.isDeviceConnected(device)) { + // not throwing an exception as there could be a race between a connection (server-side, + // notification of connection in flight) and a mute operation (client-side) + Log.i(TAG, "muteAwaitConnection ignored, device (" + device + ") already connected"); + return; + } + synchronized (mMuteAwaitConnectionLock) { + if (mMutingExpectedDevice != null) { + Log.e(TAG, "muteAwaitConnection ignored, another in progress for device:" + + mMutingExpectedDevice); + throw new IllegalStateException("muteAwaitConnection already in progress"); + } + mMutingExpectedDevice = device; + mMutedUsagesAwaitingConnection = usages; + mPlaybackMonitor.muteAwaitConnection(usages, device, timeOutMs); + } + dispatchMuteAwaitConnection(cb -> { try { + cb.dispatchOnMutedUntilConnection(device, usages); } catch (RemoteException e) { } }); + } + + /** @see AudioManager#getMutingExpectedDevice */ + public @Nullable AudioDeviceAttributes getMutingExpectedDevice() { + enforceModifyAudioRoutingPermission(); + synchronized (mMuteAwaitConnectionLock) { + return mMutingExpectedDevice; + } + } + + /** @see AudioManager#cancelMuteAwaitConnection */ + @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection + public void cancelMuteAwaitConnection(@NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(device); + enforceModifyAudioRoutingPermission(); + Log.i(TAG, "cancelMuteAwaitConnection for device:" + device); + final int[] mutedUsages; + synchronized (mMuteAwaitConnectionLock) { + if (mMutingExpectedDevice == null) { + // not throwing an exception as there could be a race between a timeout + // (server-side) and a cancel operation (client-side) + Log.i(TAG, "cancelMuteAwaitConnection ignored, no expected device"); + return; + } + if (!device.equals(mMutingExpectedDevice)) { + Log.e(TAG, "cancelMuteAwaitConnection ignored, got " + device + + "] but expected device is" + mMutingExpectedDevice); + throw new IllegalStateException("cancelMuteAwaitConnection for wrong device"); + } + mutedUsages = mMutedUsagesAwaitingConnection; + mMutingExpectedDevice = null; + mMutedUsagesAwaitingConnection = null; + mPlaybackMonitor.cancelMuteAwaitConnection(); + } + dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent( + AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, device, mutedUsages); + } catch (RemoteException e) { } }); + } + + final RemoteCallbackList mMuteAwaitConnectionDispatchers = + new RemoteCallbackList(); + + /** @see AudioManager#registerMuteAwaitConnectionCallback */ + public void registerMuteAwaitConnectionDispatcher(@NonNull IMuteAwaitConnectionCallback cb, + boolean register) { + enforceModifyAudioRoutingPermission(); + if (register) { + mMuteAwaitConnectionDispatchers.register(cb); + } else { + mMuteAwaitConnectionDispatchers.unregister(cb); + } + } + + @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection + void checkMuteAwaitConnection() { + final AudioDeviceAttributes device; + final int[] mutedUsages; + synchronized (mMuteAwaitConnectionLock) { + if (mMutingExpectedDevice == null) { + return; + } + device = mMutingExpectedDevice; + mutedUsages = mMutedUsagesAwaitingConnection; + if (!mDeviceBroker.isDeviceConnected(device)) { + return; + } + mMutingExpectedDevice = null; + mMutedUsagesAwaitingConnection = null; + Log.i(TAG, "muteAwaitConnection device " + device + " connected, unmuting"); + mPlaybackMonitor.cancelMuteAwaitConnection(); + } + dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent( + AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION, device, mutedUsages); + } catch (RemoteException e) { } }); + } + + /** + * Called by PlaybackActivityMonitor when the timeout hit for the mute on device connection + */ + @SuppressLint("EmptyCatch") // callback exception caught inside dispatchMuteAwaitConnection + void onMuteAwaitConnectionTimeout(@NonNull AudioDeviceAttributes timedOutDevice) { + final int[] mutedUsages; + synchronized (mMuteAwaitConnectionLock) { + if (!timedOutDevice.equals(mMutingExpectedDevice)) { + return; + } + Log.i(TAG, "muteAwaitConnection timeout, clearing expected device " + + mMutingExpectedDevice); + mutedUsages = mMutedUsagesAwaitingConnection; + mMutingExpectedDevice = null; + mMutedUsagesAwaitingConnection = null; + } + dispatchMuteAwaitConnection(cb -> { try { + cb.dispatchOnUnmutedEvent( + AudioManager.MuteAwaitConnectionCallback.EVENT_TIMEOUT, + timedOutDevice, mutedUsages); + } catch (RemoteException e) { } }); + } + + private void dispatchMuteAwaitConnection( + java.util.function.Consumer callback) { + final int nbDispatchers = mMuteAwaitConnectionDispatchers.beginBroadcast(); + // lazy initialization as errors unlikely + ArrayList errorList = null; + for (int i = 0; i < nbDispatchers; i++) { + try { + callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i)); + } catch (Exception e) { + if (errorList == null) { + errorList = new ArrayList<>(1); + } + errorList.add(mMuteAwaitConnectionDispatchers.getBroadcastItem(i)); + } + } + if (errorList != null) { + for (IMuteAwaitConnectionCallback errorItem : errorList) { + mMuteAwaitConnectionDispatchers.unregister(errorItem); + } + } + mMuteAwaitConnectionDispatchers.finishBroadcast(); + } + + //========================================================================================== // Device orientation //========================================================================================== diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index b94cea4d5d401..b333ed24eb1df 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -17,9 +17,11 @@ package com.android.server.audio; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; import android.media.AudioSystem; @@ -27,21 +29,27 @@ import android.media.IPlaybackConfigDispatcher; import android.media.PlayerBase; import android.media.VolumeShaper; import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; +import android.os.Message; import android.os.RemoteException; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; import java.io.PrintWriter; import java.text.DateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.function.Consumer; /** * Class to receive and dispatch updates from AudioSystem about recording configurations. @@ -54,6 +62,7 @@ public final class PlaybackActivityMonitor /*package*/ static final boolean DEBUG = false; /*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1; /*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2; + /*package*/ static final int VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID = 3; private static final VolumeShaper.Configuration DUCK_VSHAPE = new VolumeShaper.Configuration.Builder() @@ -73,6 +82,18 @@ public final class PlaybackActivityMonitor .createIfNeeded() .build(); + private static final long UNMUTE_DURATION_MS = 100; + private static final VolumeShaper.Configuration MUTE_AWAIT_CONNECTION_VSHAPE = + new VolumeShaper.Configuration.Builder() + .setId(VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID) + .setCurve(new float[] { 0.f, 1.f } /* times */, + new float[] { 1.f, 0.f } /* volumes */) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + // even though we specify a duration, it's only used for the unmute, + // for muting this volume shaper is run with PLAY_SKIP_RAMP + .setDuration(UNMUTE_DURATION_MS) + .build(); + // TODO support VolumeShaper on those players private static final int[] UNDUCKABLE_PLAYER_TYPES = { AudioPlaybackConfiguration.PLAYER_TYPE_AAUDIO, @@ -90,6 +111,7 @@ public final class PlaybackActivityMonitor private boolean mHasPublicClients = false; private final Object mPlayerLock = new Object(); + @GuardedBy("mPlayerLock") private final HashMap mPlayers = new HashMap(); @@ -97,12 +119,16 @@ public final class PlaybackActivityMonitor private int mSavedAlarmVolume = -1; private final int mMaxAlarmVolume; private int mPrivilegedAlarmActiveCount = 0; + private final Consumer mMuteAwaitConnectionTimeoutCb; - PlaybackActivityMonitor(Context context, int maxAlarmVolume) { + PlaybackActivityMonitor(Context context, int maxAlarmVolume, + Consumer muteTimeoutCallback) { mContext = context; mMaxAlarmVolume = maxAlarmVolume; PlayMonitorClient.sListenerDeathMonitor = this; AudioPlaybackConfiguration.sPlayerDeathMonitor = this; + mMuteAwaitConnectionTimeoutCb = muteTimeoutCallback; + initEventHandler(); } //================================================================= @@ -170,6 +196,7 @@ public final class PlaybackActivityMonitor sEventLogger.log(new NewPlayerEvent(apc)); synchronized(mPlayerLock) { mPlayers.put(newPiid, apc); + maybeMutePlayerAwaitingConnection(apc); } return newPiid; } @@ -323,6 +350,7 @@ public final class PlaybackActivityMonitor mPlayers.remove(new Integer(piid)); mDuckingManager.removeReleased(apc); mFadingManager.removeReleased(apc); + mMutedPlayersAwaitingConnection.remove(Integer.valueOf(piid)); checkVolumeForPrivilegedAlarm(apc, AudioPlaybackConfiguration.PLAYER_STATE_RELEASED); change = apc.handleStateEvent(AudioPlaybackConfiguration.PLAYER_STATE_RELEASED, AudioPlaybackConfiguration.PLAYER_DEVICEID_INVALID); @@ -451,7 +479,7 @@ public final class PlaybackActivityMonitor pw.println("\n faded out players piids:"); mFadingManager.dump(pw); // players muted due to the device ringing or being in a call - pw.print("\n muted player piids:"); + pw.print("\n muted player piids due to call/ring:"); for (int piid : mMutedPlayers) { pw.print(" " + piid); } @@ -462,6 +490,12 @@ public final class PlaybackActivityMonitor pw.print(" " + uid); } pw.println("\n"); + // muted players: + pw.print("\n muted players (piids) awaiting device connection: BL3 ####"); + for (int piid : mMutedPlayersAwaitingConnection) { + pw.print(" " + piid); + } + pw.println("\n"); // log sEventLogger.dump(pw); } @@ -1100,6 +1134,155 @@ public final class PlaybackActivityMonitor } } + private static final class MuteAwaitConnectionEvent extends AudioEventLogger.Event { + private final @NonNull int[] mUsagesToMute; + + MuteAwaitConnectionEvent(@NonNull int[] usagesToMute) { + mUsagesToMute = usagesToMute; + } + + @Override + public String eventToString() { + return "muteAwaitConnection muting usages " + Arrays.toString(mUsagesToMute); + } + } + static final AudioEventLogger sEventLogger = new AudioEventLogger(100, "playback activity as reported through PlayerBase"); + + //========================================================================================== + // Mute conditional on device connection + //========================================================================================== + void muteAwaitConnection(@NonNull int[] usagesToMute, + @NonNull AudioDeviceAttributes dev, long timeOutMs) { + synchronized (mPlayerLock) { + mutePlayersExpectingDevice(usagesToMute); + // schedule timeout (remove previously scheduled first) + mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION); + mEventHandler.sendMessageDelayed( + mEventHandler.obtainMessage(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION, dev), + timeOutMs); + } + } + + void cancelMuteAwaitConnection() { + synchronized (mPlayerLock) { + // cancel scheduled timeout, ignore device, only one expected device at a time + mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION); + // unmute immediately + unmutePlayersExpectingDevice(); + } + } + + /** + * List of the piids of the players that are muted until a specific audio device connects + */ + @GuardedBy("mPlayerLock") + private final ArrayList mMutedPlayersAwaitingConnection = new ArrayList(); + + /** + * List of AudioAttributes usages to mute until a specific audio device connects + */ + @GuardedBy("mPlayerLock") + private @Nullable int[] mMutedUsagesAwaitingConnection = null; + + @GuardedBy("mPlayerLock") + private void mutePlayersExpectingDevice(@NonNull int[] usagesToMute) { + sEventLogger.log(new MuteAwaitConnectionEvent(usagesToMute)); + mMutedUsagesAwaitingConnection = usagesToMute; + final Set piidSet = mPlayers.keySet(); + final Iterator piidIterator = piidSet.iterator(); + // find which players to mute + while (piidIterator.hasNext()) { + final Integer piid = piidIterator.next(); + final AudioPlaybackConfiguration apc = mPlayers.get(piid); + if (apc == null) { + continue; + } + maybeMutePlayerAwaitingConnection(apc); + } + } + + @GuardedBy("mPlayerLock") + private void maybeMutePlayerAwaitingConnection(@NonNull AudioPlaybackConfiguration apc) { + if (mMutedUsagesAwaitingConnection == null) { + return; + } + for (int usage : mMutedUsagesAwaitingConnection) { + if (usage == apc.getAudioAttributes().getUsage()) { + try { + sEventLogger.log((new AudioEventLogger.StringEvent( + "awaiting connection: muting piid:" + + apc.getPlayerInterfaceId() + + " uid:" + apc.getClientUid())).printLog(TAG)); + apc.getPlayerProxy().applyVolumeShaper( + MUTE_AWAIT_CONNECTION_VSHAPE, + PLAY_CREATE_IF_NEEDED); + mMutedPlayersAwaitingConnection.add(apc.getPlayerInterfaceId()); + } catch (Exception e) { + Log.e(TAG, "awaiting connection: error muting player " + + apc.getPlayerInterfaceId(), e); + } + } + } + } + + @GuardedBy("mPlayerLock") + private void unmutePlayersExpectingDevice() { + if (mMutedPlayersAwaitingConnection.isEmpty()) { + return; + } + for (int piid : mMutedPlayersAwaitingConnection) { + final AudioPlaybackConfiguration apc = mPlayers.get(piid); + if (apc == null) { + continue; + } + try { + sEventLogger.log(new AudioEventLogger.StringEvent( + "unmuting piid:" + piid).printLog(TAG)); + apc.getPlayerProxy().applyVolumeShaper(MUTE_AWAIT_CONNECTION_VSHAPE, + VolumeShaper.Operation.REVERSE); + } catch (Exception e) { + Log.e(TAG, "Error unmuting player " + piid + " uid:" + + apc.getClientUid(), e); + } + } + mMutedPlayersAwaitingConnection.clear(); + mMutedUsagesAwaitingConnection = null; + } + + //================================================================= + // Message handling + private Handler mEventHandler; + private HandlerThread mEventThread; + + /** + * timeout for a mute awaiting a device connection + * args: + * msg.obj: the audio device being expected + * type: AudioDeviceAttributes + */ + private static final int MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION = 1; + + private void initEventHandler() { + mEventThread = new HandlerThread(TAG); + mEventThread.start(); + mEventHandler = new Handler(mEventThread.getLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION: + Log.i(TAG, "Timeout for muting waiting for " + + (AudioDeviceAttributes) msg.obj + ", unmuting"); + synchronized (mPlayerLock) { + unmutePlayersExpectingDevice(); + } + mMuteAwaitConnectionTimeoutCb.accept((AudioDeviceAttributes) msg.obj); + break; + default: + break; + } + } + }; + } } From 4da775ddb8d44abc4aa223a8f2ef88bd96781bb4 Mon Sep 17 00:00:00 2001 From: Jean-Michel Trivi Date: Fri, 3 Dec 2021 15:33:46 -0800 Subject: [PATCH 2/2] AudioManager: test API for device connection Add a test API to simulate the connection/disconnection of an audio device. The simulation of the connection goes as far as AudioService, but doesn't go into the lower layers of the native audio policy or the HAL. Bug: 200218901 Test: atest com.google.android.gts.audioservice.AudioServiceHostTest#testMuteAwaitConnection Change-Id: I36ae9e07b4c7408a0deb471e9b89263c4db21591 --- core/api/test-current.txt | 1 + media/java/android/media/AudioManager.java | 17 +++++++++++ media/java/android/media/IAudioService.aidl | 2 ++ .../server/audio/AudioDeviceBroker.java | 10 ++++++- .../server/audio/AudioDeviceInventory.java | 29 +++++++++++++++---- .../android/server/audio/AudioService.java | 14 +++++++++ 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 10146731491c1..fee4ed85cf4d8 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1458,6 +1458,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable(); method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int requestAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String, int, int); method public void setRampingRingerEnabled(boolean); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setTestDeviceConnectionState(@NonNull android.media.AudioDeviceAttributes, boolean); } public static final class AudioRecord.MetricsConstants { diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 9bde8c3e59ccb..d721291ad78f4 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -5775,6 +5775,23 @@ public class AudioManager { } } + /** + * Indicate wired accessory connection state change. + * @param device {@link AudioDeviceAttributes} of the device to "fake-connect" + * @param connected true for connected, false for disconnected + * {@hide} + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device, + boolean connected) { + try { + getService().setTestDeviceConnectionState(device, connected); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Indicate Bluetooth profile connection state change. * Configuration changes for A2DP are indicated by having the same newDevice and diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 67f366473134e..afcbc5769cf0c 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -458,4 +458,6 @@ interface IAudioService { void registerMuteAwaitConnectionDispatcher(in IMuteAwaitConnectionCallback cb, boolean register); + + void setTestDeviceConnectionState(in AudioDeviceAttributes device, boolean connected); } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 3bd723e1187cc..e4ac7be6be590 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -508,6 +508,13 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + /*package*/ void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device, + @AudioService.ConnectionState int state) { + synchronized (mDeviceStateLock) { + mDeviceInventory.setTestDeviceConnectionState(device, state); + } + } + /*package*/ static final class BleVolumeInfo { final int mIndex; final int mMaxIndex; @@ -1008,7 +1015,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address, String deviceName) { synchronized (mDeviceStateLock) { - return mDeviceInventory.handleDeviceConnection(connect, device, address, deviceName); + return mDeviceInventory.handleDeviceConnection(connect, device, address, deviceName, + false /*for test*/); } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index fdb2970f6339f..a27e4b77959c2 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -224,6 +224,7 @@ public class AudioDeviceInventory { public final String mAddress; public final String mName; public final String mCaller; + public boolean mForTest = false; /*package*/ WiredDeviceConnectionState(int type, @AudioService.ConnectionState int state, String address, String name, String caller) { @@ -521,7 +522,7 @@ public class AudioDeviceInventory { } if (!handleDeviceConnection(wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, - wdcs.mType, wdcs.mAddress, wdcs.mName)) { + wdcs.mType, wdcs.mAddress, wdcs.mName, wdcs.mForTest)) { // change of connection state failed, bailout mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed") .record(); @@ -681,7 +682,7 @@ public class AudioDeviceInventory { * @param device the device whose connection state is queried * @return true if connected */ - @GuardedBy("AudioDeviceBroker.mDeviceStateLock") + // called with AudioDeviceBroker.mDeviceStateLock lock held public boolean isDeviceConnected(@NonNull AudioDeviceAttributes device) { final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(), device.getAddress()); @@ -696,10 +697,12 @@ public class AudioDeviceInventory { * @param device the device type * @param address the address of the device * @param deviceName human-readable name of device + * @param isForTesting if true, not calling AudioSystem for the connection as this is + * just for testing * @return false if an error was reported by AudioSystem */ /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address, - String deviceName) { + String deviceName, boolean isForTesting) { if (AudioService.DEBUG_DEVICES) { Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:" + Integer.toHexString(device) + " address:" + address @@ -722,9 +725,14 @@ public class AudioDeviceInventory { Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected); } if (connect && !isConnected) { - final int res = mAudioSystem.setDeviceConnectionState(device, - AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName, - AudioSystem.AUDIO_FORMAT_DEFAULT); + final int res; + if (isForTesting) { + res = AudioSystem.AUDIO_STATUS_OK; + } else { + res = mAudioSystem.setDeviceConnectionState(device, + AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName, + AudioSystem.AUDIO_FORMAT_DEFAULT); + } if (res != AudioSystem.AUDIO_STATUS_OK) { final String reason = "not connecting device 0x" + Integer.toHexString(device) + " due to command error " + res; @@ -930,6 +938,15 @@ public class AudioDeviceInventory { } } + /*package*/ void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device, + @AudioService.ConnectionState int state) { + final WiredDeviceConnectionState connection = new WiredDeviceConnectionState( + device.getInternalType(), state, device.getAddress(), + "test device", "com.android.server.audio"); + connection.mForTest = true; + onSetWiredDeviceConnectionState(connection); + } + //------------------------------------------------------------------- // Internal utilities diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3eeffcb82e27a..aa33644357be3 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6364,6 +6364,20 @@ public class AudioService extends IAudioService.Stub mDeviceBroker.setWiredDeviceConnectionState(type, state, address, name, caller); } + /** @see AudioManager#setTestDeviceConnectionState(AudioDeviceAttributes, boolean) */ + public void setTestDeviceConnectionState(@NonNull AudioDeviceAttributes device, + boolean connected) { + Objects.requireNonNull(device); + enforceModifyAudioRoutingPermission(); + mDeviceBroker.setTestDeviceConnectionState(device, + connected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED); + // simulate a routing update from native + sendMsg(mAudioHandler, + MSG_ROUTING_UPDATED, + SENDMSG_REPLACE, 0, 0, null, + /*delay*/ 0); + } + /** * @hide * The states that can be used with AudioService.setBluetoothHearingAidDeviceConnectionState()