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/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 46aad3f11ac8d..d721291ad78f4 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. @@ -5774,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 @@ -7967,6 +7985,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..afcbc5769cf0c 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,16 @@ 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); + + void setTestDeviceConnectionState(in AudioDeviceAttributes device, boolean connected); } 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..e4ac7be6be590 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) { @@ -502,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; @@ -1002,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 f32d3b52d61cc..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(); @@ -593,7 +594,7 @@ public class AudioDeviceInventory { } //------------------------------------------------------------ - // + // preferred device(s) /*package*/ int setPreferredDevicesForStrategySync(int strategy, @NonNull List devices) { @@ -674,16 +675,34 @@ 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 + */ + // called with AudioDeviceBroker.mDeviceStateLock lock held + 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 * @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 @@ -706,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; @@ -914,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 9c8a663545b75..aa33644357be3 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(); @@ -6357,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() @@ -7652,7 +7673,6 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect); if (mHasSpatializerEffect) { mSpatializerHelper.setFeatureEnabled(isSpatialAudioEnabled()); - monitorRoutingChanges(true); } mAudioEventWakeLock.release(); break; @@ -7791,7 +7811,7 @@ public class AudioService extends IAudioService.Stub break; case MSG_ROUTING_UPDATED: - mSpatializerHelper.onRoutingUpdated(); + onRoutingUpdatedFromAudioThread(); break; case MSG_PERSIST_SPATIAL_AUDIO_ENABLED: @@ -8611,6 +8631,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; + } + } + }; + } }