diff --git a/core/api/current.txt b/core/api/current.txt index 74d313c636b75..65d3e4a36b9de 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -19373,14 +19373,17 @@ package android.media { public class AudioManager { method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener); method public int abandonAudioFocusRequest(@NonNull android.media.AudioFocusRequest); + method public void addOnCommunicationDeviceChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnCommunicationDeviceChangedListener); method public void adjustStreamVolume(int, int, int); method public void adjustSuggestedStreamVolume(int, int, int); method public void adjustVolume(int, int); + method public void clearDeviceForCommunication(); method public void dispatchMediaKeyEvent(android.view.KeyEvent); method public int generateAudioSessionId(); method @NonNull public java.util.List getActivePlaybackConfigurations(); method @NonNull public java.util.List getActiveRecordingConfigurations(); method public int getAllowedCapturePolicy(); + method @Nullable public android.media.AudioDeviceInfo getDeviceForCommunication(); method public android.media.AudioDeviceInfo[] getDevices(int); method public java.util.List getMicrophones() throws java.io.IOException; method public int getMode(); @@ -19415,11 +19418,13 @@ package android.media { method @Deprecated public void registerMediaButtonEventReceiver(android.app.PendingIntent); method @Deprecated public void registerRemoteControlClient(android.media.RemoteControlClient); method @Deprecated public boolean registerRemoteController(android.media.RemoteController); + method public void removeOnCommunicationDeviceChangedListener(@NonNull android.media.AudioManager.OnCommunicationDeviceChangedListener); method @Deprecated public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, int, int); method public int requestAudioFocus(@NonNull android.media.AudioFocusRequest); method public void setAllowedCapturePolicy(int); method @Deprecated public void setBluetoothA2dpOn(boolean); method public void setBluetoothScoOn(boolean); + method public boolean setDeviceForCommunication(@NonNull android.media.AudioDeviceInfo); method public void setMicrophoneMute(boolean); method public void setMode(int); method public void setParameters(String); @@ -19554,6 +19559,10 @@ package android.media { method public void onAudioFocusChange(int); } + public static interface AudioManager.OnCommunicationDeviceChangedListener { + method public void onCommunicationDeviceChanged(@Nullable android.media.AudioDeviceInfo); + } + public final class AudioMetadata { method @NonNull public static android.media.AudioMetadataMap createMap(); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index b6bd687a3a88c..9cf9ce45602b8 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -850,6 +850,7 @@ package android.media { } public class AudioManager { + method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int); method public boolean hasRegisteredDynamicPolicy(); } diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 1ca45fe9f70b1..20e64ffe94712 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -294,20 +294,25 @@ static sp setJniCallback(JNIEnv* env, return old; } -#define check_AudioSystem_Command(status) _check_AudioSystem_Command(__func__, (status)) +#define check_AudioSystem_Command(...) _check_AudioSystem_Command(__func__, __VA_ARGS__) -static int _check_AudioSystem_Command(const char* caller, status_t status) -{ - ALOGE_IF(status, "Command failed for %s: %d", caller, status); +static int _check_AudioSystem_Command(const char *caller, status_t status, + std::vector ignoredErrors = {}) { + int jniStatus = kAudioStatusOk; switch (status) { case DEAD_OBJECT: - return kAudioStatusMediaServerDied; + jniStatus = kAudioStatusMediaServerDied; + break; case NO_ERROR: - return kAudioStatusOk; + break; default: + if (std::find(begin(ignoredErrors), end(ignoredErrors), status) == end(ignoredErrors)) { + jniStatus = kAudioStatusError; + } break; } - return kAudioStatusError; + ALOGE_IF(jniStatus != kAudioStatusOk, "Command failed for %s: %d", caller, status); + return jniStatus; } static jint getVectorOfAudioDeviceTypeAddr(JNIEnv *env, jintArray deviceTypes, @@ -2381,9 +2386,12 @@ static jint android_media_AudioSystem_setDevicesRoleForStrategy(JNIEnv *env, job static jint android_media_AudioSystem_removeDevicesRoleForStrategy(JNIEnv *env, jobject thiz, jint strategy, jint role) { - return (jint)check_AudioSystem_Command( - AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)strategy, - (device_role_t)role)); + return (jint) + check_AudioSystem_Command(AudioSystem::removeDevicesRoleForStrategy((product_strategy_t) + strategy, + (device_role_t) + role), + {NAME_NOT_FOUND}); } static jint android_media_AudioSystem_getDevicesForRoleAndStrategy(JNIEnv *env, jobject thiz, diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java index 6c8b50037d3d0..7caac899a6034 100644 --- a/media/java/android/media/AudioDeviceAttributes.java +++ b/media/java/android/media/AudioDeviceAttributes.java @@ -120,7 +120,13 @@ public final class AudioDeviceAttributes implements Parcelable { mAddress = address; } - /*package*/ AudioDeviceAttributes(int nativeType, @NonNull String address) { + /** + * @hide + * Constructor from internal device type and address + * @param type the internal device type, as defined in {@link AudioSystem} + * @param address the address of the device, or an empty string for devices without one + */ + public AudioDeviceAttributes(int nativeType, @NonNull String address) { mRole = (nativeType & AudioSystem.DEVICE_BIT_IN) != 0 ? ROLE_INPUT : ROLE_OUTPUT; mType = AudioDeviceInfo.convertInternalDeviceToDeviceType(nativeType); mAddress = address; @@ -191,10 +197,8 @@ public final class AudioDeviceAttributes implements Parcelable { public String toString() { return new String("AudioDeviceAttributes:" + " role:" + roleToString(mRole) - + " type:" + (mRole == ROLE_OUTPUT ? AudioSystem.getOutputDeviceName( - AudioDeviceInfo.convertDeviceTypeToInternalDevice(mType)) - : AudioSystem.getInputDeviceName( - AudioDeviceInfo.convertDeviceTypeToInternalDevice(mType))) + + " type:" + (mRole == ROLE_OUTPUT ? AudioSystem.getOutputDeviceName(mNativeType) + : AudioSystem.getInputDeviceName(mNativeType)) + " addr:" + mAddress); } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index ed9e5175fb781..7dff0c2b93806 100755 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -6123,6 +6123,29 @@ public class AudioManager { return infoListFromPortList(ports, flags); } + /** + * Returns an {@link AudioDeviceInfo} corresponding to the specified {@link AudioPort} ID. + * @param portId The audio port ID to look up for. + * @param flags A set of bitflags specifying the criteria to test. + * @see #GET_DEVICES_OUTPUTS + * @see #GET_DEVICES_INPUTS + * @see #GET_DEVICES_ALL + * @return An AudioDeviceInfo or null if no device with matching port ID is found. + * @hide + */ + public static AudioDeviceInfo getDeviceForPortId(int portId, int flags) { + if (portId == 0) { + return null; + } + AudioDeviceInfo[] devices = getDevicesStatic(flags); + for (AudioDeviceInfo device : devices) { + if (device.getId() == portId) { + return device; + } + } + return null; + } + /** * Registers an {@link AudioDeviceCallback} object to receive notifications of changes * to the set of connected audio devices. @@ -6666,6 +6689,297 @@ public class AudioManager { } } + /** + * Selects the audio device that should be used for communication use cases, for instance voice + * or video calls. This method can be used by voice or video chat applications to select a + * different audio device than the one selected by default by the platform. + *

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

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

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

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

This API replaces the following deprecated APIs: + *

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

Example

+ *

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

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

This API replaces the following deprecated APIs: + *

    + *
  • {@link #isBluetoothScoOn()} + *
  • {@link #isSpeakerphoneOn()} + *
+ * @return an {@link AudioDeviceInfo} indicating which audio device is + * currently selected or communication use cases or null if default selection + * is used. + */ + @Nullable + public AudioDeviceInfo getDeviceForCommunication() { + try { + return getDeviceForPortId( + getService().getDeviceForCommunication(), GET_DEVICES_OUTPUTS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Returns an {@link AudioDeviceInfo} corresponding to a connected device of the type provided. + * The type must be a valid output type defined in AudioDeviceInfo class, + * for instance {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. + * The method will return null if no device of the provided type is connected. + * If more than one device of the provided type is connected, an object corresponding to the + * first device encountered in the enumeration list will be returned. + * @param deviceType The device device for which an AudioDeviceInfo + * object is queried. + * @return An AudioDeviceInfo object or null if no device with the requested type is connected. + * @throws IllegalArgumentException If an invalid device type is specified. + */ + @TestApi + @Nullable + public static AudioDeviceInfo getDeviceInfoFromType( + @AudioDeviceInfo.AudioDeviceTypeOut int deviceType) { + AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo device : devices) { + if (device.getType() == deviceType) { + return device; + } + } + return null; + } + + /** + * Listener registered by client to be notified upon communication audio device change. + * See {@link #setDeviceForCommunication(AudioDeviceInfo)}. + */ + public interface OnCommunicationDeviceChangedListener { + /** + * Callback method called upon communication audio device change. + * @param device the audio device selected for communication use cases + */ + void onCommunicationDeviceChanged(@Nullable AudioDeviceInfo device); + } + + /** + * Adds a listener for being notified of changes to the communication audio device. + * See {@link #setDeviceForCommunication(AudioDeviceInfo)}. + * @param executor + * @param listener + */ + public void addOnCommunicationDeviceChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnCommunicationDeviceChangedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mCommDevListenerLock) { + if (hasCommDevListener(listener)) { + throw new IllegalArgumentException( + "attempt to call addOnCommunicationDeviceChangedListener() " + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (mCommDevListeners == null) { + mCommDevListeners = new ArrayList<>(); + } + final int oldCbCount = mCommDevListeners.size(); + mCommDevListeners.add(new CommDevListenerInfo(listener, executor)); + if (oldCbCount == 0 && mCommDevListeners.size() > 0) { + // register binder for callbacks + if (mCommDevDispatcherStub == null) { + mCommDevDispatcherStub = new CommunicationDeviceDispatcherStub(); + } + try { + getService().registerCommunicationDeviceDispatcher(mCommDevDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * Removes a previously added listener of changes to the communication audio device. + * See {@link #setDeviceForCommunication(AudioDeviceInfo)}. + * @param listener + */ + public void removeOnCommunicationDeviceChangedListener( + @NonNull OnCommunicationDeviceChangedListener listener) { + Objects.requireNonNull(listener); + synchronized (mCommDevListenerLock) { + if (!removeCommDevListener(listener)) { + throw new IllegalArgumentException( + "attempt to call removeOnCommunicationDeviceChangedListener() " + + "on an unregistered listener"); + } + if (mCommDevListeners.size() == 0) { + // unregister binder for callbacks + try { + getService().unregisterCommunicationDeviceDispatcher( + mCommDevDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + mCommDevDispatcherStub = null; + mCommDevListeners = null; + } + } + } + } + + private final Object mCommDevListenerLock = new Object(); + /** + * List of listeners for preferred device for strategy and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mCommDevListenerLock") + private @Nullable ArrayList mCommDevListeners; + + private static class CommDevListenerInfo { + final @NonNull OnCommunicationDeviceChangedListener mListener; + final @NonNull Executor mExecutor; + + CommDevListenerInfo(OnCommunicationDeviceChangedListener listener, Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + @GuardedBy("mCommDevListenerLock") + private CommunicationDeviceDispatcherStub mCommDevDispatcherStub; + + private final class CommunicationDeviceDispatcherStub + extends ICommunicationDeviceDispatcher.Stub { + + @Override + public void dispatchCommunicationDeviceChanged(int portId) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList commDevListeners; + synchronized (mCommDevListenerLock) { + if (mCommDevListeners == null || mCommDevListeners.size() == 0) { + return; + } + commDevListeners = (ArrayList) mCommDevListeners.clone(); + } + AudioDeviceInfo device = getDeviceForPortId(portId, GET_DEVICES_OUTPUTS); + final long ident = Binder.clearCallingIdentity(); + try { + for (CommDevListenerInfo info : commDevListeners) { + info.mExecutor.execute(() -> + info.mListener.onCommunicationDeviceChanged(device)); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + @GuardedBy("mCommDevListenerLock") + private @Nullable CommDevListenerInfo getCommDevListenerInfo( + OnCommunicationDeviceChangedListener listener) { + if (mCommDevListeners == null) { + return null; + } + for (CommDevListenerInfo info : mCommDevListeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + @GuardedBy("mCommDevListenerLock") + private boolean hasCommDevListener(OnCommunicationDeviceChangedListener listener) { + return getCommDevListenerInfo(listener) != null; + } + + @GuardedBy("mCommDevListenerLock") + /** + * @return true if the listener was removed from the list + */ + private boolean removeCommDevListener(OnCommunicationDeviceChangedListener listener) { + final CommDevListenerInfo infoToRemove = getCommDevListenerInfo(listener); + if (infoToRemove != null) { + mCommDevListeners.remove(infoToRemove); + return true; + } + return false; + } + //--------------------------------------------------------- // Inner classes //-------------------- diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index ef6ba065f4147..18c8a72b165bb 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1712,7 +1712,7 @@ public class AudioSystem int[] types = new int[devices.size()]; String[] addresses = new String[devices.size()]; for (int i = 0; i < devices.size(); ++i) { - types[i] = AudioDeviceInfo.convertDeviceTypeToInternalDevice(devices.get(i).getType()); + types[i] = devices.get(i).getInternalType(); addresses[i] = devices.get(i).getAddress(); } return setDevicesRoleForStrategy(strategy, role, types, addresses); diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index d9b44cdd20e75..ebaa3162d0e46 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -27,6 +27,7 @@ import android.media.IAudioFocusDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; import android.media.ICapturePresetDevicesRoleDispatcher; +import android.media.ICommunicationDeviceDispatcher; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; @@ -320,4 +321,13 @@ interface IAudioService { oneway void unregisterCapturePresetDevicesRoleDispatcher( ICapturePresetDevicesRoleDispatcher dispatcher); + + boolean setDeviceForCommunication(IBinder cb, int portId); + + int getDeviceForCommunication(); + + void registerCommunicationDeviceDispatcher(ICommunicationDeviceDispatcher dispatcher); + + oneway void unregisterCommunicationDeviceDispatcher( + ICommunicationDeviceDispatcher dispatcher); } diff --git a/media/java/android/media/ICommunicationDeviceDispatcher.aidl b/media/java/android/media/ICommunicationDeviceDispatcher.aidl new file mode 100644 index 0000000000000..429f934a77dc1 --- /dev/null +++ b/media/java/android/media/ICommunicationDeviceDispatcher.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +/** + * AIDL for AudioService to signal audio communication device updates. + * + * {@hide} + */ +oneway interface ICommunicationDeviceDispatcher { + + void dispatchCommunicationDeviceChanged(int portId); + +} diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 1615998f77875..26f5c4ca1e802 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -16,6 +16,7 @@ package com.android.server.audio; import android.annotation.NonNull; +import android.annotation.Nullable; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; @@ -25,20 +26,26 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; import android.media.ICapturePresetDevicesRoleDispatcher; +import android.media.ICommunicationDeviceDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.MediaMetrics; +import android.media.audiopolicy.AudioProductStrategy; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemClock; +import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.util.PrintWriterPrinter; @@ -46,8 +53,9 @@ import android.util.PrintWriterPrinter; import com.android.internal.annotations.GuardedBy; import java.io.PrintWriter; -import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; @@ -72,11 +80,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private final @NonNull Context mContext; /** Forced device usage for communications sent to AudioSystem */ - private int mForcedUseForComm; - /** - * Externally reported force device usage state returned by getters: always consistent - * with requests by setters */ - private int mForcedUseForCommExt; + private AudioDeviceAttributes mPreferredDeviceforComm; + private int mCommunicationStrategyId = -1; // Manages all connected devices, only ever accessed on the message loop private final AudioDeviceInventory mDeviceInventory; @@ -132,11 +137,23 @@ import java.util.concurrent.atomic.AtomicBoolean; init(); } + private void initCommunicationStrategyId() { + List strategies = AudioProductStrategy.getAudioProductStrategies(); + for (AudioProductStrategy strategy : strategies) { + if (strategy.getAudioAttributesForLegacyStreamType(AudioSystem.STREAM_VOICE_CALL) + != null) { + mCommunicationStrategyId = strategy.getId(); + return; + } + } + mCommunicationStrategyId = -1; + } + private void init() { setupMessaging(mContext); - mForcedUseForComm = AudioSystem.FORCE_NONE; - mForcedUseForCommExt = mForcedUseForComm; + mPreferredDeviceforComm = null; + initCommunicationStrategyId(); } /*package*/ Context getContext() { @@ -159,15 +176,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } /*package*/ void onAudioServerDied() { - // Restore forced usage for communications and record - synchronized (mDeviceStateLock) { - AudioSystem.setParameters( - "BT_SCO=" + (mForcedUseForComm == AudioSystem.FORCE_BT_SCO ? "on" : "off")); - onSetForceUse(AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, - false /*fromA2dp*/, "onAudioServerDied"); - onSetForceUse(AudioSystem.FOR_RECORD, mForcedUseForComm, - false /*fromA2dp*/, "onAudioServerDied"); - } // restore devices sendMsgNoDelay(MSG_RESTORE_DEVICES, SENDMSG_REPLACE); } @@ -219,85 +227,210 @@ import java.util.concurrent.atomic.AtomicBoolean; * Turns speakerphone on/off * @param on * @param eventSource for logging purposes - * @return true if speakerphone state changed */ - /*package*/ boolean setSpeakerphoneOn(IBinder cb, int pid, boolean on, String eventSource) { - synchronized (mDeviceStateLock) { - if (!addSpeakerphoneClient(cb, pid, on)) { - return false; + /*package*/ void setSpeakerphoneOn(IBinder cb, int pid, boolean on, String eventSource) { + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setSpeakerphoneOn, on: " + on + " pid: " + pid); + } + + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + AudioDeviceAttributes device = null; + if (on) { + device = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""); + } else { + CommunicationRouteClient client = getCommunicationRouteClientForPid(pid); + if (client == null || !client.requestsSpeakerphone()) { + return; + } + } + setCommunicationRouteForClient( + cb, pid, device, BtHelper.SCO_MODE_UNDEFINED, eventSource); } - if (on) { - // Cancel BT SCO ON request by this same client: speakerphone and BT SCO routes - // are mutually exclusive. - // See symmetrical operation for startBluetoothScoForClient_Sync(). - mBtHelper.stopBluetoothScoForPid(pid); - } - final boolean wasOn = isSpeakerphoneOn(); - updateSpeakerphoneOn(eventSource); - return (wasOn != isSpeakerphoneOn()); } } /** - * Turns speakerphone off for a given pid and update speakerphone state. - * @param pid + * Select device for use for communication use cases. + * @param cb Client binder for death detection + * @param pid Client pid + * @param device Device selected or null to unselect. + * @param eventSource for logging purposes */ + /*package*/ boolean setDeviceForCommunication( + IBinder cb, int pid, AudioDeviceInfo device, String eventSource) { + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setDeviceForCommunication, device: " + device + ", pid: " + pid); + } + + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + AudioDeviceAttributes deviceAttr = null; + if (device != null) { + deviceAttr = new AudioDeviceAttributes(device); + } else { + CommunicationRouteClient client = getCommunicationRouteClientForPid(pid); + if (client == null) { + return false; + } + } + setCommunicationRouteForClient( + cb, pid, deviceAttr, BtHelper.SCO_MODE_UNDEFINED, eventSource); + } + } + return true; + } + @GuardedBy("mDeviceStateLock") - private void setSpeakerphoneOffForPid(int pid) { - SpeakerphoneClient client = getSpeakerphoneClientForPid(pid); + /*package*/ void setCommunicationRouteForClient( + IBinder cb, int pid, AudioDeviceAttributes device, + int scoAudioMode, String eventSource) { + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setCommunicationRouteForClient: device: " + device); + } + AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent( + "setCommunicationRouteForClient for pid: " + pid + + " device: " + device + + " from API: " + eventSource)).printLog(TAG)); + + final boolean wasBtScoRequested = isBluetoothScoRequested(); + final boolean wasSpeakerphoneRequested = isSpeakerphoneRequested(); + CommunicationRouteClient client; + + + // Save previous client route in case of failure to start BT SCO audio + AudioDeviceAttributes prevClientDevice = null; + client = getCommunicationRouteClientForPid(pid); + if (client != null) { + prevClientDevice = client.getDevice(); + } + + if (device != null) { + client = addCommunicationRouteClient(cb, pid, device); + if (client == null) { + Log.w(TAG, "setCommunicationRouteForClient: could not add client for pid: " + + pid + " and device: " + device); + } + } else { + client = removeCommunicationRouteClient(cb, true); + } if (client == null) { return; } - client.unregisterDeathRecipient(); - mSpeakerphoneClients.remove(client); - final String eventSource = new StringBuilder("setSpeakerphoneOffForPid(") - .append(pid).append(")").toString(); - updateSpeakerphoneOn(eventSource); - } - @GuardedBy("mDeviceStateLock") - private void updateSpeakerphoneOn(String eventSource) { - if (isSpeakerphoneOnRequested()) { - if (mForcedUseForComm == AudioSystem.FORCE_BT_SCO) { - setForceUse_Async(AudioSystem.FOR_RECORD, AudioSystem.FORCE_NONE, eventSource); + boolean isBtScoRequested = isBluetoothScoRequested(); + if (isBtScoRequested && !wasBtScoRequested) { + if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) { + Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for pid: " + + pid); + // clean up or restore previous client selection + if (prevClientDevice != null) { + addCommunicationRouteClient(cb, pid, prevClientDevice); + } else { + removeCommunicationRouteClient(cb, true); + } + postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); } - mForcedUseForComm = AudioSystem.FORCE_SPEAKER; - } else if (mForcedUseForComm == AudioSystem.FORCE_SPEAKER) { - if (mBtHelper.isBluetoothScoOn()) { - mForcedUseForComm = AudioSystem.FORCE_BT_SCO; - setForceUse_Async( - AudioSystem.FOR_RECORD, AudioSystem.FORCE_BT_SCO, eventSource); - } else { - mForcedUseForComm = AudioSystem.FORCE_NONE; + } else if (!isBtScoRequested && wasBtScoRequested) { + mBtHelper.stopBluetoothSco(eventSource); + } + + if (wasSpeakerphoneRequested != isSpeakerphoneRequested()) { + try { + mContext.sendBroadcastAsUser( + new Intent(AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED) + .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY), UserHandle.ALL); + } catch (Exception e) { + Log.w(TAG, "failed to broadcast ACTION_SPEAKERPHONE_STATE_CHANGED: " + e); } } - mForcedUseForCommExt = mForcedUseForComm; - setForceUse_Async(AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, eventSource); + + sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE, SENDMSG_QUEUE, eventSource); } /** - * Returns if speakerphone is requested ON or OFF. - * If the current audio mode owner is in the speakerphone client list, use this preference. + * Returns the device currently requested for communication use case. + * If the current audio mode owner is in the communication route client list, + * use this preference. * Otherwise use first client's preference (first client corresponds to latest request). - * Speakerphone is requested OFF if no client is in the list. - * @return true if speakerphone is requested ON, false otherwise + * null is returned if no client is in the list. + * @return AudioDeviceAttributes the requested device for communication. */ + @GuardedBy("mDeviceStateLock") - private boolean isSpeakerphoneOnRequested() { - if (mSpeakerphoneClients.isEmpty()) { - return false; - } - for (SpeakerphoneClient cl : mSpeakerphoneClients) { + private AudioDeviceAttributes requestedCommunicationDevice() { + AudioDeviceAttributes device = null; + for (CommunicationRouteClient cl : mCommunicationRouteClients) { if (cl.getPid() == mModeOwnerPid) { - return cl.isOn(); + device = cl.getDevice(); } } - return mSpeakerphoneClients.get(0).isOn(); + if (!mCommunicationRouteClients.isEmpty() && mModeOwnerPid == 0) { + device = mCommunicationRouteClients.get(0).getDevice(); + } + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "requestedCommunicationDevice, device: " + + device + " mode owner pid: " + mModeOwnerPid); + } + return device; } - /*package*/ boolean isSpeakerphoneOn() { + /** + * Returns the device currently requested for communication use case. + * @return AudioDeviceInfo the requested device for communication. + */ + AudioDeviceInfo getDeviceForCommunication() { synchronized (mDeviceStateLock) { - return (mForcedUseForCommExt == AudioSystem.FORCE_SPEAKER); + AudioDeviceAttributes device = requestedCommunicationDevice(); + if (device == null) { + return null; + } + return AudioManager.getDeviceInfoFromType(device.getType()); + } + } + + /** + * Helper method on top of requestedCommunicationDevice() indicating if + * speakerphone ON is currently requested or not. + * @return true if speakerphone ON requested, false otherwise. + */ + + private boolean isSpeakerphoneRequested() { + synchronized (mDeviceStateLock) { + AudioDeviceAttributes device = requestedCommunicationDevice(); + return device != null + && device.getType() + == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; + } + } + + /** + * Indicates if active route selection for communication is speakerphone. + * @return true if speakerphone is active, false otherwise. + */ + /*package*/ boolean isSpeakerphoneOn() { + AudioDeviceAttributes device = getPreferredDeviceForComm(); + if (device == null) { + return false; + } + return device.getInternalType() == AudioSystem.DEVICE_OUT_SPEAKER; + } + + /** + * Helper method on top of requestedCommunicationDevice() indicating if + * Bluetooth SCO ON is currently requested or not. + * @return true if Bluetooth SCO ON is requested, false otherwise. + */ + /*package*/ boolean isBluetoothScoRequested() { + synchronized (mDeviceStateLock) { + AudioDeviceAttributes device = requestedCommunicationDevice(); + return device != null + && device.getType() + == AudioDeviceInfo.TYPE_BLUETOOTH_SCO; } } @@ -348,7 +481,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - /*package*/ void postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, int profile, boolean suppressNoisyIntent, int a2dpVolume) { @@ -431,42 +563,32 @@ import java.util.concurrent.atomic.AtomicBoolean; sendLMsgNoDelay(MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info); } - // never called by system components - /*package*/ void setBluetoothScoOnByApp(boolean on) { - synchronized (mDeviceStateLock) { - mForcedUseForCommExt = on ? AudioSystem.FORCE_BT_SCO : AudioSystem.FORCE_NONE; - } - } - /*package*/ boolean isBluetoothScoOnForApp() { - synchronized (mDeviceStateLock) { - return mForcedUseForCommExt == AudioSystem.FORCE_BT_SCO; - } - } + /** + * Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn(). + */ + private boolean mBluetoothScoOn; /*package*/ void setBluetoothScoOn(boolean on, String eventSource) { - //Log.i(TAG, "setBluetoothScoOn: " + on + " " + eventSource); - synchronized (mDeviceStateLock) { - if (on) { - // do not accept SCO ON if SCO audio is not connected - if (!mBtHelper.isBluetoothScoOn()) { - mForcedUseForCommExt = AudioSystem.FORCE_BT_SCO; - return; - } - mForcedUseForComm = AudioSystem.FORCE_BT_SCO; - } else if (mForcedUseForComm == AudioSystem.FORCE_BT_SCO) { - mForcedUseForComm = isSpeakerphoneOnRequested() - ? AudioSystem.FORCE_SPEAKER : AudioSystem.FORCE_NONE; - } - mForcedUseForCommExt = mForcedUseForComm; - AudioSystem.setParameters("BT_SCO=" + (on ? "on" : "off")); - sendIILMsgNoDelay(MSG_IIL_SET_FORCE_USE, SENDMSG_QUEUE, - AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, eventSource); - sendIILMsgNoDelay(MSG_IIL_SET_FORCE_USE, SENDMSG_QUEUE, - AudioSystem.FOR_RECORD, mForcedUseForComm, eventSource); + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setBluetoothScoOn: " + on + " " + eventSource); } - // Un-mute ringtone stream volume - mAudioService.postUpdateRingerModeServiceInt(); + synchronized (mDeviceStateLock) { + mBluetoothScoOn = on; + sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE, SENDMSG_QUEUE, eventSource); + } + } + + /** + * Indicates if active route selection for communication is Bluetooth SCO. + * @return true if Bluetooth SCO is active , false otherwise. + */ + /*package*/ boolean isBluetoothScoOn() { + AudioDeviceAttributes device = getPreferredDeviceForComm(); + if (device == null) { + return false; + } + return AudioSystem.DEVICE_OUT_ALL_SCO_SET.contains(device.getInternalType()); } /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) { @@ -509,22 +631,38 @@ import java.util.concurrent.atomic.AtomicBoolean; sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, device); } - @GuardedBy("mSetModeLock") - /*package*/ void startBluetoothScoForClient_Sync(IBinder cb, int scoAudioMode, + /*package*/ void startBluetoothScoForClient(IBinder cb, int pid, int scoAudioMode, @NonNull String eventSource) { - synchronized (mDeviceStateLock) { - // Cancel speakerphone ON request by this same client: speakerphone and BT SCO routes - // are mutually exclusive. - // See symmetrical operation for setSpeakerphoneOn(true). - setSpeakerphoneOffForPid(Binder.getCallingPid()); - mBtHelper.startBluetoothScoForClient(cb, scoAudioMode, eventSource); + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "startBluetoothScoForClient_Sync, pid: " + pid); + } + + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + AudioDeviceAttributes device = + new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""); + setCommunicationRouteForClient(cb, pid, device, scoAudioMode, eventSource); + } } } - @GuardedBy("mSetModeLock") - /*package*/ void stopBluetoothScoForClient_Sync(IBinder cb, @NonNull String eventSource) { - synchronized (mDeviceStateLock) { - mBtHelper.stopBluetoothScoForClient(cb, eventSource); + /*package*/ void stopBluetoothScoForClient( + IBinder cb, int pid, @NonNull String eventSource) { + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "stopBluetoothScoForClient_Sync, pid: " + pid); + } + + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + CommunicationRouteClient client = getCommunicationRouteClientForPid(pid); + if (client == null || !client.requestsBluetoothSco()) { + return; + } + setCommunicationRouteForClient( + cb, pid, null, BtHelper.SCO_MODE_UNDEFINED, eventSource); + } } } @@ -533,10 +671,19 @@ import java.util.concurrent.atomic.AtomicBoolean; return mDeviceInventory.setPreferredDevicesForStrategySync(strategy, devices); } + /*package*/ void postSetPreferredDevicesForStrategy(int strategy, + @NonNull List devices) { + sendILMsgNoDelay(MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY, SENDMSG_REPLACE, strategy, devices); + } + /*package*/ int removePreferredDevicesForStrategySync(int strategy) { return mDeviceInventory.removePreferredDevicesForStrategySync(strategy); } + /*package*/ void postRemovePreferredDevicesForStrategy(int strategy) { + sendIMsgNoDelay(MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_REPLACE, strategy); + } + /*package*/ void registerStrategyPreferredDevicesDispatcher( @NonNull IStrategyPreferredDevicesDispatcher dispatcher) { mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher); @@ -566,6 +713,45 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.unregisterCapturePresetDevicesRoleDispatcher(dispatcher); } + /*package*/ void registerCommunicationDeviceDispatcher( + @NonNull ICommunicationDeviceDispatcher dispatcher) { + mCommDevDispatchers.register(dispatcher); + } + + /*package*/ void unregisterCommunicationDeviceDispatcher( + @NonNull ICommunicationDeviceDispatcher dispatcher) { + mCommDevDispatchers.unregister(dispatcher); + } + + // Monitoring of communication device + final RemoteCallbackList mCommDevDispatchers = + new RemoteCallbackList(); + + // portId of the device currently selected for communication: avoids broadcasting changes + // when same communication route is applied + @GuardedBy("mDeviceStateLock") + int mCurCommunicationPortId = -1; + + @GuardedBy("mDeviceStateLock") + private void dispatchCommunicationDevice() { + AudioDeviceInfo device = getDeviceForCommunication(); + int portId = (getDeviceForCommunication() == null) ? 0 : device.getId(); + if (portId == mCurCommunicationPortId) { + return; + } + mCurCommunicationPortId = portId; + + final int nbDispatchers = mCommDevDispatchers.beginBroadcast(); + for (int i = 0; i < nbDispatchers; i++) { + try { + mCommDevDispatchers.getBroadcastItem(i) + .dispatchCommunicationDeviceChanged(portId); + } catch (RemoteException e) { + } + } + mCommDevDispatchers.finishBroadcast(); + } + //--------------------------------------------------------------------- // Communication with (to) AudioService //TODO check whether the AudioService methods are candidates to move here @@ -696,12 +882,8 @@ import java.util.concurrent.atomic.AtomicBoolean; hearingAidProfile); } - /*package*/ void postScoClientDied(Object obj) { - sendLMsgNoDelay(MSG_L_SCOCLIENT_DIED, SENDMSG_QUEUE, obj); - } - - /*package*/ void postSpeakerphoneClientDied(Object obj) { - sendLMsgNoDelay(MSG_L_SPEAKERPHONE_CLIENT_DIED, SENDMSG_QUEUE, obj); + /*package*/ void postCommunicationRouteClientDied(CommunicationRouteClient client) { + sendLMsgNoDelay(MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED, SENDMSG_QUEUE, client); } /*package*/ void postSaveSetPreferredDevicesForStrategy(int strategy, @@ -821,15 +1003,17 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.dump(pw, prefix); - pw.println("\n" + prefix + "mForcedUseForComm: " - + AudioSystem.forceUseConfigToString(mForcedUseForComm)); - pw.println(prefix + "mForcedUseForCommExt: " - + AudioSystem.forceUseConfigToString(mForcedUseForCommExt)); - pw.println(prefix + "mModeOwnerPid: " + mModeOwnerPid); - pw.println(prefix + "Speakerphone clients:"); - mSpeakerphoneClients.forEach((cl) -> { - pw.println(" " + prefix + "pid: " + cl.getPid() + " on: " - + cl.isOn() + " cb: " + cl.getBinder()); }); + pw.println("\n" + prefix + "Communication route clients:"); + mCommunicationRouteClients.forEach((cl) -> { + pw.println(" " + prefix + "pid: " + cl.getPid() + " device: " + + cl.getDevice() + " cb: " + cl.getBinder()); }); + + pw.println("\n" + prefix + "mPreferredDeviceforComm: " + + mPreferredDeviceforComm); + pw.println(prefix + "mCommunicationStrategyId: " + + mCommunicationStrategyId); + + pw.println("\n" + prefix + "mModeOwnerPid: " + mModeOwnerPid); mBtHelper.dump(pw, prefix); } @@ -852,6 +1036,11 @@ import java.util.concurrent.atomic.AtomicBoolean; .set(MediaMetrics.Property.FORCE_USE_MODE, AudioSystem.forceUseConfigToString(config)) .record(); + + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "onSetForceUse(useCase<" + useCase + ">, config<" + config + ">, fromA2dp<" + + fromA2dp + ">, eventSource<" + eventSource + ">)"); + } AudioSystem.setForceUse(useCase, config); } @@ -917,9 +1106,13 @@ import java.util.concurrent.atomic.AtomicBoolean; public void handleMessage(Message msg) { switch (msg.what) { case MSG_RESTORE_DEVICES: - synchronized (mDeviceStateLock) { - mDeviceInventory.onRestoreDevices(); - mBtHelper.onAudioServerDiedRestoreA2dp(); + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + initCommunicationStrategyId(); + mDeviceInventory.onRestoreDevices(); + mBtHelper.onAudioServerDiedRestoreA2dp(); + onUpdateCommunicationRoute("MSG_RESTORE_DEVICES"); + } } break; case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: @@ -1009,25 +1202,24 @@ import java.util.concurrent.atomic.AtomicBoolean; if (mModeOwnerPid != msg.arg1) { mModeOwnerPid = msg.arg1; if (msg.arg2 != AudioSystem.MODE_RINGTONE) { - updateSpeakerphoneOn("setNewModeOwner"); - } - if (mModeOwnerPid != 0) { - mBtHelper.disconnectBluetoothSco(mModeOwnerPid); + onUpdateCommunicationRoute("setNewModeOwner"); } } } } break; - case MSG_L_SCOCLIENT_DIED: + case MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED: synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { - mBtHelper.scoClientDied(msg.obj); + onCommunicationRouteClientDied((CommunicationRouteClient) msg.obj); } } break; - case MSG_L_SPEAKERPHONE_CLIENT_DIED: - synchronized (mDeviceStateLock) { - speakerphoneClientDied(msg.obj); + case MSG_L_UPDATE_COMMUNICATION_ROUTE: + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + onUpdateCommunicationRoute((String) msg.obj); + } } break; case MSG_TOGGLE_HDMI: @@ -1126,6 +1318,17 @@ import java.util.concurrent.atomic.AtomicBoolean; final int strategy = msg.arg1; mDeviceInventory.onSaveRemovePreferredDevices(strategy); } break; + case MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY: { + final int strategy = msg.arg1; + final List devices = + (List) msg.obj; + setPreferredDevicesForStrategySync(strategy, devices); + + } break; + case MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY: { + final int strategy = msg.arg1; + removePreferredDevicesForStrategySync(strategy); + } break; case MSG_CHECK_MUTE_MUSIC: checkMessagesMuteMusic(0); break; @@ -1207,18 +1410,19 @@ import java.util.concurrent.atomic.AtomicBoolean; // process external command to (dis)connect a hearing aid device private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31; - // a ScoClient died in BtHelper - private static final int MSG_L_SCOCLIENT_DIED = 32; - private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY = 33; - private static final int MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY = 34; + private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY = 32; + private static final int MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY = 33; - private static final int MSG_L_SPEAKERPHONE_CLIENT_DIED = 35; - private static final int MSG_CHECK_MUTE_MUSIC = 36; - private static final int MSG_REPORT_NEW_ROUTES_A2DP = 37; + private static final int MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED = 34; + private static final int MSG_CHECK_MUTE_MUSIC = 35; + private static final int MSG_REPORT_NEW_ROUTES_A2DP = 36; - private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET = 38; - private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 39; + private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET = 37; + private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38; + private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE = 39; + private static final int MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY = 40; + private static final int MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY = 41; private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { @@ -1372,14 +1576,20 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private class SpeakerphoneClient implements IBinder.DeathRecipient { + // List of applications requesting a specific route for communication. + @GuardedBy("mDeviceStateLock") + private final @NonNull LinkedList mCommunicationRouteClients = + new LinkedList(); + + private class CommunicationRouteClient implements IBinder.DeathRecipient { private final IBinder mCb; private final int mPid; - private final boolean mOn; - SpeakerphoneClient(IBinder cb, int pid, boolean on) { + private AudioDeviceAttributes mDevice; + + CommunicationRouteClient(IBinder cb, int pid, AudioDeviceAttributes device) { mCb = cb; mPid = pid; - mOn = on; + mDevice = device; } public boolean registerDeathRecipient() { @@ -1388,7 +1598,7 @@ import java.util.concurrent.atomic.AtomicBoolean; mCb.linkToDeath(this, 0); status = true; } catch (RemoteException e) { - Log.w(TAG, "SpeakerphoneClient could not link to " + mCb + " binder death"); + Log.w(TAG, "CommunicationRouteClient could not link to " + mCb + " binder death"); } return status; } @@ -1397,13 +1607,13 @@ import java.util.concurrent.atomic.AtomicBoolean; try { mCb.unlinkToDeath(this, 0); } catch (NoSuchElementException e) { - Log.w(TAG, "SpeakerphoneClient could not not unregistered to binder"); + Log.w(TAG, "CommunicationRouteClient could not not unregistered to binder"); } } @Override public void binderDied() { - postSpeakerphoneClientDied(this); + postCommunicationRouteClientDied(this); } IBinder getBinder() { @@ -1414,29 +1624,103 @@ import java.util.concurrent.atomic.AtomicBoolean; return mPid; } - boolean isOn() { - return mOn; + AudioDeviceAttributes getDevice() { + return mDevice; + } + + boolean requestsBluetoothSco() { + return mDevice != null + && mDevice.getType() + == AudioDeviceInfo.TYPE_BLUETOOTH_SCO; + } + + boolean requestsSpeakerphone() { + return mDevice != null + && mDevice.getType() + == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; } } + // @GuardedBy("mSetModeLock") @GuardedBy("mDeviceStateLock") - private void speakerphoneClientDied(Object obj) { - if (obj == null) { + private void onCommunicationRouteClientDied(CommunicationRouteClient client) { + if (client == null) { return; } Log.w(TAG, "Speaker client died"); - if (removeSpeakerphoneClient(((SpeakerphoneClient) obj).getBinder(), false) != null) { - updateSpeakerphoneOn("speakerphoneClientDied"); + if (removeCommunicationRouteClient(client.getBinder(), false) + != null) { + onUpdateCommunicationRoute("onCommunicationRouteClientDied"); } } - private SpeakerphoneClient removeSpeakerphoneClient(IBinder cb, boolean unregister) { - for (SpeakerphoneClient cl : mSpeakerphoneClients) { + /** + * Determines which forced usage for communication should be sent to audio policy manager + * as a function of current SCO audio activation state and active communication route requests. + * SCO audio state has the highest priority as it can result from external activation by + * telephony service. + * @return selected forced usage for communication. + */ + @GuardedBy("mDeviceStateLock") + @Nullable private AudioDeviceAttributes getPreferredDeviceForComm() { + boolean btSCoOn = mBluetoothScoOn && mBtHelper.isBluetoothScoOn(); + if (btSCoOn) { + // Use the SCO device known to BtHelper so that it matches exactly + // what has been communicated to audio policy manager. The device + // returned by requestedCommunicationDevice() can be a dummy SCO device if legacy + // APIs are used to start SCO audio. + AudioDeviceAttributes device = mBtHelper.getHeadsetAudioDevice(); + if (device != null) { + return device; + } + } + AudioDeviceAttributes device = requestedCommunicationDevice(); + if (device == null + || AudioSystem.DEVICE_OUT_ALL_SCO_SET.contains(device.getInternalType())) { + // Do not indicate BT SCO selection if SCO is requested but SCO is not ON + return null; + } + return device; + } + + /** + * Configures audio policy manager and audio HAL according to active communication route. + * Always called from message Handler. + */ + // @GuardedBy("mSetModeLock") + @GuardedBy("mDeviceStateLock") + private void onUpdateCommunicationRoute(String eventSource) { + mPreferredDeviceforComm = getPreferredDeviceForComm(); + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "onUpdateCommunicationRoute, mPreferredDeviceforComm: " + + mPreferredDeviceforComm + " eventSource: " + eventSource); + } + + if (mPreferredDeviceforComm == null + || !AudioSystem.DEVICE_OUT_ALL_SCO_SET.contains( + mPreferredDeviceforComm.getInternalType())) { + AudioSystem.setParameters("BT_SCO=off"); + } else { + AudioSystem.setParameters("BT_SCO=on"); + } + if (mPreferredDeviceforComm == null) { + postRemovePreferredDevicesForStrategy(mCommunicationStrategyId); + } else { + postSetPreferredDevicesForStrategy( + mCommunicationStrategyId, Arrays.asList(mPreferredDeviceforComm)); + } + mAudioService.postUpdateRingerModeServiceInt(); + dispatchCommunicationDevice(); + } + + private CommunicationRouteClient removeCommunicationRouteClient( + IBinder cb, boolean unregister) { + for (CommunicationRouteClient cl : mCommunicationRouteClients) { if (cl.getBinder() == cb) { if (unregister) { cl.unregisterDeathRecipient(); } - mSpeakerphoneClients.remove(cl); + mCommunicationRouteClients.remove(cl); return cl; } } @@ -1444,30 +1728,25 @@ import java.util.concurrent.atomic.AtomicBoolean; } @GuardedBy("mDeviceStateLock") - private boolean addSpeakerphoneClient(IBinder cb, int pid, boolean on) { + private CommunicationRouteClient addCommunicationRouteClient( + IBinder cb, int pid, AudioDeviceAttributes device) { // always insert new request at first position - removeSpeakerphoneClient(cb, true); - SpeakerphoneClient client = new SpeakerphoneClient(cb, pid, on); + removeCommunicationRouteClient(cb, true); + CommunicationRouteClient client = new CommunicationRouteClient(cb, pid, device); if (client.registerDeathRecipient()) { - mSpeakerphoneClients.add(0, client); - return true; + mCommunicationRouteClients.add(0, client); + return client; } - return false; + return null; } @GuardedBy("mDeviceStateLock") - private SpeakerphoneClient getSpeakerphoneClientForPid(int pid) { - for (SpeakerphoneClient cl : mSpeakerphoneClients) { + private CommunicationRouteClient getCommunicationRouteClientForPid(int pid) { + for (CommunicationRouteClient cl : mCommunicationRouteClients) { if (cl.getPid() == pid) { return cl; } } return null; } - - // List of clients requesting speakerPhone ON - @GuardedBy("mDeviceStateLock") - private final @NonNull ArrayList mSpeakerphoneClients = - new ArrayList(); - } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 33a8a30243de0..82586b8f9b235 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -648,6 +648,10 @@ public class AudioDeviceInventory { /*package*/ int setPreferredDevicesForStrategySync(int strategy, @NonNull List devices) { final long identity = Binder.clearCallingIdentity(); + + AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent( + "setPreferredDevicesForStrategySync, strategy: " + strategy + + " devices: " + devices)).printLog(TAG)); final int status = mAudioSystem.setDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a75d65040a6b9..024dca7e23c6e 100755 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -85,6 +85,7 @@ import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; import android.media.IAudioService; import android.media.ICapturePresetDevicesRoleDispatcher; +import android.media.ICommunicationDeviceDispatcher; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; @@ -207,6 +208,9 @@ public class AudioService extends IAudioService.Stub /** debug calls to devices APIs */ protected static final boolean DEBUG_DEVICES = false; + /** Debug communication route */ + protected static final boolean DEBUG_COMM_RTE = false; + /** How long to delay before persisting a change in volume/ringer mode. */ private static final int PERSIST_DELAY = 500; @@ -3689,7 +3693,7 @@ public class AudioService extends IAudioService.Stub final boolean ringerModeMute = ringerMode == AudioManager.RINGER_MODE_VIBRATE || ringerMode == AudioManager.RINGER_MODE_SILENT; final boolean shouldRingSco = ringerMode == AudioManager.RINGER_MODE_VIBRATE - && isBluetoothScoOn(); + && mDeviceBroker.isBluetoothScoOn(); // Ask audio policy engine to force use Bluetooth SCO channel if needed final String eventSource = "muteRingerModeStreams() from u/pid:" + Binder.getCallingUid() + "/" + Binder.getCallingPid(); @@ -4265,6 +4269,115 @@ public class AudioService extends IAudioService.Stub restoreDeviceVolumeBehavior(); } + private static final int[] VALID_COMMUNICATION_DEVICE_TYPES = { + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_WIRED_HEADSET, + AudioDeviceInfo.TYPE_USB_HEADSET, + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + AudioDeviceInfo.TYPE_HEARING_AID, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_USB_DEVICE, + AudioDeviceInfo.TYPE_BLE_SPEAKER, + AudioDeviceInfo.TYPE_LINE_ANALOG, + AudioDeviceInfo.TYPE_HDMI, + AudioDeviceInfo.TYPE_AUX_LINE + }; + + private boolean isValidCommunicationDevice(AudioDeviceInfo device) { + for (int type : VALID_COMMUNICATION_DEVICE_TYPES) { + if (device.getType() == type) { + return true; + } + } + return false; + } + + /** @see AudioManager#setDeviceForCommunication(int) */ + public boolean setDeviceForCommunication(IBinder cb, int portId) { + final int uid = Binder.getCallingUid(); + final int pid = Binder.getCallingPid(); + + AudioDeviceInfo device = null; + if (portId != 0) { + device = AudioManager.getDeviceForPortId(portId, AudioManager.GET_DEVICES_OUTPUTS); + if (device == null) { + throw new IllegalArgumentException("invalid portID " + portId); + } + if (!isValidCommunicationDevice(device)) { + throw new IllegalArgumentException("invalid device type " + device.getType()); + } + } + final String eventSource = new StringBuilder("setDeviceForCommunication(") + .append(") from u/pid:").append(uid).append("/") + .append(pid).toString(); + + int deviceType = AudioSystem.DEVICE_OUT_DEFAULT; + String deviceAddress = null; + if (device != null) { + deviceType = device.getPort().type(); + deviceAddress = device.getAddress(); + } else { + AudioDeviceInfo curDevice = mDeviceBroker.getDeviceForCommunication(); + if (curDevice != null) { + deviceType = curDevice.getPort().type(); + deviceAddress = curDevice.getAddress(); + } + } + // do not log metrics if clearing communication device while no communication device + // was selected + if (deviceType != AudioSystem.DEVICE_OUT_DEFAULT) { + new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + + MediaMetrics.SEPARATOR + "setDeviceForCommunication") + .set(MediaMetrics.Property.DEVICE, + AudioSystem.getDeviceName(deviceType)) + .set(MediaMetrics.Property.ADDRESS, deviceAddress) + .set(MediaMetrics.Property.STATE, device != null + ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED) + .record(); + } + + final long ident = Binder.clearCallingIdentity(); + boolean status = + mDeviceBroker.setDeviceForCommunication(cb, pid, device, eventSource); + Binder.restoreCallingIdentity(ident); + return status; + } + + /** @see AudioManager#getDeviceForCommunication() */ + public int getDeviceForCommunication() { + final long ident = Binder.clearCallingIdentity(); + AudioDeviceInfo device = mDeviceBroker.getDeviceForCommunication(); + Binder.restoreCallingIdentity(ident); + if (device == null) { + return 0; + } + return device.getId(); + } + + /** @see AudioManager#addOnCommunicationDeviceChangedListener( + * Executor, AudioManager.OnCommunicationDeviceChangedListener) + */ + public void registerCommunicationDeviceDispatcher( + @Nullable ICommunicationDeviceDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + mDeviceBroker.registerCommunicationDeviceDispatcher(dispatcher); + } + + /** @see AudioManager#removeOnCommunicationDeviceChangedListener( + * AudioManager.OnCommunicationDeviceChangedListener) + */ + public void unregisterCommunicationDeviceDispatcher( + @Nullable ICommunicationDeviceDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + mDeviceBroker.unregisterCommunicationDeviceDispatcher(dispatcher); + } + /** @see AudioManager#setSpeakerphoneOn(boolean) */ public void setSpeakerphoneOn(IBinder cb, boolean on) { if (!checkAudioSettingsPermission("setSpeakerphoneOn()")) { @@ -4274,10 +4387,10 @@ public class AudioService extends IAudioService.Stub // for logging only final int uid = Binder.getCallingUid(); final int pid = Binder.getCallingPid(); + final String eventSource = new StringBuilder("setSpeakerphoneOn(").append(on) .append(") from u/pid:").append(uid).append("/") .append(pid).toString(); - final boolean stateChanged = mDeviceBroker.setSpeakerphoneOn(cb, pid, on, eventSource); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR + "setSpeakerphoneOn") .setUid(uid) @@ -4285,17 +4398,9 @@ public class AudioService extends IAudioService.Stub .set(MediaMetrics.Property.STATE, on ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF) .record(); - - if (stateChanged) { - final long ident = Binder.clearCallingIdentity(); - try { - mContext.sendBroadcastAsUser( - new Intent(AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED) - .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY), UserHandle.ALL); - } finally { - Binder.restoreCallingIdentity(ident); - } - } + final long ident = Binder.clearCallingIdentity(); + mDeviceBroker.setSpeakerphoneOn(cb, pid, on, eventSource); + Binder.restoreCallingIdentity(ident); } /** @see AudioManager#isSpeakerphoneOn() */ @@ -4303,6 +4408,11 @@ public class AudioService extends IAudioService.Stub return mDeviceBroker.isSpeakerphoneOn(); } + + /** BT SCO audio state seen by apps using the deprecated API setBluetoothScoOn(). + * @see isBluetoothScoOn() */ + private boolean mBtScoOnByApp; + /** @see AudioManager#setBluetoothScoOn(boolean) */ public void setBluetoothScoOn(boolean on) { if (!checkAudioSettingsPermission("setBluetoothScoOn()")) { @@ -4311,7 +4421,7 @@ public class AudioService extends IAudioService.Stub // Only enable calls from system components if (UserHandle.getCallingAppId() >= FIRST_APPLICATION_UID) { - mDeviceBroker.setBluetoothScoOnByApp(on); + mBtScoOnByApp = on; return; } @@ -4337,7 +4447,7 @@ public class AudioService extends IAudioService.Stub * Note that it doesn't report internal state, but state seen by apps (which may have * called setBluetoothScoOn() */ public boolean isBluetoothScoOn() { - return mDeviceBroker.isBluetoothScoOnForApp(); + return mBtScoOnByApp || mDeviceBroker.isBluetoothScoOn(); } // TODO investigate internal users due to deprecation of SDK API @@ -4384,7 +4494,7 @@ public class AudioService extends IAudioService.Stub .set(MediaMetrics.Property.SCO_AUDIO_MODE, BtHelper.scoAudioModeToString(scoAudioMode)) .record(); - startBluetoothScoInt(cb, scoAudioMode, eventSource); + startBluetoothScoInt(cb, pid, scoAudioMode, eventSource); } @@ -4403,10 +4513,10 @@ public class AudioService extends IAudioService.Stub .set(MediaMetrics.Property.SCO_AUDIO_MODE, BtHelper.scoAudioModeToString(BtHelper.SCO_MODE_VIRTUAL_CALL)) .record(); - startBluetoothScoInt(cb, BtHelper.SCO_MODE_VIRTUAL_CALL, eventSource); + startBluetoothScoInt(cb, pid, BtHelper.SCO_MODE_VIRTUAL_CALL, eventSource); } - void startBluetoothScoInt(IBinder cb, int scoAudioMode, @NonNull String eventSource) { + void startBluetoothScoInt(IBinder cb, int pid, int scoAudioMode, @NonNull String eventSource) { MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH) .set(MediaMetrics.Property.EVENT, "startBluetoothScoInt") .set(MediaMetrics.Property.SCO_AUDIO_MODE, @@ -4417,9 +4527,9 @@ public class AudioService extends IAudioService.Stub mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission or systemReady").record(); return; } - synchronized (mDeviceBroker.mSetModeLock) { - mDeviceBroker.startBluetoothScoForClient_Sync(cb, scoAudioMode, eventSource); - } + final long ident = Binder.clearCallingIdentity(); + mDeviceBroker.startBluetoothScoForClient(cb, pid, scoAudioMode, eventSource); + Binder.restoreCallingIdentity(ident); mmi.record(); } @@ -4434,9 +4544,9 @@ public class AudioService extends IAudioService.Stub final String eventSource = new StringBuilder("stopBluetoothSco()") .append(") from u/pid:").append(uid).append("/") .append(pid).toString(); - synchronized (mDeviceBroker.mSetModeLock) { - mDeviceBroker.stopBluetoothScoForClient_Sync(cb, eventSource); - } + final long ident = Binder.clearCallingIdentity(); + mDeviceBroker.stopBluetoothScoForClient(cb, pid, eventSource); + Binder.restoreCallingIdentity(ident); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH) .setUid(uid) .setPid(pid) @@ -4862,8 +4972,7 @@ public class AudioService extends IAudioService.Stub switch (mPlatformType) { case AudioSystem.PLATFORM_VOICE: if (isInCommunication()) { - if (AudioSystem.getForceUse(AudioSystem.FOR_COMMUNICATION) - == AudioSystem.FORCE_BT_SCO) { + if (mDeviceBroker.isBluetoothScoOn()) { // Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO..."); return AudioSystem.STREAM_BLUETOOTH_SCO; } else { @@ -4899,8 +5008,7 @@ public class AudioService extends IAudioService.Stub } default: if (isInCommunication()) { - if (AudioSystem.getForceUse(AudioSystem.FOR_COMMUNICATION) - == AudioSystem.FORCE_BT_SCO) { + if (mDeviceBroker.isBluetoothScoOn()) { if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO"); return AudioSystem.STREAM_BLUETOOTH_SCO; } else { @@ -7670,6 +7778,7 @@ public class AudioService extends IAudioService.Stub pw.print(" mHasVibrator="); pw.println(mHasVibrator); pw.print(" mVolumePolicy="); pw.println(mVolumePolicy); pw.print(" mAvrcpAbsVolSupported="); pw.println(mAvrcpAbsVolSupported); + pw.print(" mBtScoOnByApp="); pw.println(mBtScoOnByApp); pw.print(" mIsSingleVolume="); pw.println(mIsSingleVolume); pw.print(" mUseFixedVolume="); pw.println(mUseFixedVolume); pw.print(" mFixedVolumeDevices="); pw.println(dumpDeviceTypes(mFixedVolumeDevices)); diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 7616557ac80f3..c9a1fcf76f5b7 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -27,11 +27,10 @@ import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; import android.content.Intent; +import android.media.AudioDeviceAttributes; import android.media.AudioManager; import android.media.AudioSystem; import android.os.Binder; -import android.os.IBinder; -import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; @@ -39,9 +38,7 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.List; -import java.util.NoSuchElementException; import java.util.Objects; /** @@ -58,10 +55,6 @@ public class BtHelper { mDeviceBroker = broker; } - // List of clients having issued a SCO start request - @GuardedBy("BtHelper.this") - private final @NonNull ArrayList mScoClients = new ArrayList(); - // BluetoothHeadset API to control SCO connection private @Nullable BluetoothHeadset mBluetoothHeadset; @@ -301,6 +294,8 @@ public class BtHelper { @GuardedBy("AudioDeviceBroker.mDeviceStateLock") /*package*/ synchronized void receiveBtEvent(Intent intent) { final String action = intent.getAction(); + + Log.i(TAG, "receiveBtEvent action: " + action + " mScoAudioState: " + mScoAudioState); if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) { BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); setBtScoActiveDevice(btDevice); @@ -308,20 +303,16 @@ public class BtHelper { boolean broadcast = false; int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR; int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); - // broadcast intent if the connection was initated by AudioService - if (!mScoClients.isEmpty() - && (mScoAudioState == SCO_STATE_ACTIVE_INTERNAL - || mScoAudioState == SCO_STATE_ACTIVATE_REQ - || mScoAudioState == SCO_STATE_DEACTIVATE_REQ - || mScoAudioState == SCO_STATE_DEACTIVATING)) { - broadcast = true; - } + Log.i(TAG, "receiveBtEvent ACTION_AUDIO_STATE_CHANGED: " + btState); switch (btState) { case BluetoothHeadset.STATE_AUDIO_CONNECTED: scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED; if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; + } else if (mDeviceBroker.isBluetoothScoRequested()) { + // broadcast intent if the connection was initated by AudioService + broadcast = true; } mDeviceBroker.setBluetoothScoOn(true, "BtHelper.receiveBtEvent"); break; @@ -333,21 +324,21 @@ public class BtHelper { // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ. // 2) If audio was connected then disconnected via Bluetooth APIs and // we still have pending activation requests by apps: this is indicated by - // state SCO_STATE_ACTIVE_EXTERNAL and the mScoClients list not empty. + // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested. if (mScoAudioState == SCO_STATE_ACTIVATE_REQ || (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL - && !mScoClients.isEmpty())) { + && mDeviceBroker.isBluetoothScoRequested())) { if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null && connectBluetoothScoAudioHelper(mBluetoothHeadset, mBluetoothHeadsetDevice, mScoAudioMode)) { mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; - broadcast = false; + scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING; + broadcast = true; break; } } - // Tear down SCO if disconnected from external - if (mScoAudioState == SCO_STATE_DEACTIVATING) { - clearAllScoClients(0, false); + if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) { + broadcast = true; } mScoAudioState = SCO_STATE_INACTIVE; break; @@ -356,11 +347,8 @@ public class BtHelper { && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; } - broadcast = false; break; default: - // do not broadcast CONNECTING or invalid state - broadcast = false; break; } if (broadcast) { @@ -386,81 +374,19 @@ public class BtHelper { == BluetoothHeadset.STATE_AUDIO_CONNECTED; } - /** - * Disconnect all SCO connections started by {@link AudioManager} except those started by - * {@param exceptPid} - * - * @param exceptPid pid whose SCO connections through {@link AudioManager} should be kept - */ // @GuardedBy("AudioDeviceBroker.mSetModeLock") @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ synchronized void disconnectBluetoothSco(int exceptPid) { - checkScoAudioState(); - if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL) { - return; - } - clearAllScoClients(exceptPid, true); - } - - // @GuardedBy("AudioDeviceBroker.mSetModeLock") - @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ synchronized void startBluetoothScoForClient(IBinder cb, int scoAudioMode, + /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode, @NonNull String eventSource) { - ScoClient client = getScoClient(cb, true); - // The calling identity must be cleared before calling ScoClient.incCount(). - // inCount() calls requestScoState() which in turn can call BluetoothHeadset APIs - // and this must be done on behalf of system server to make sure permissions are granted. - // The caller identity must be cleared after getScoClient() because it is needed if a new - // client is created. - final long ident = Binder.clearCallingIdentity(); - try { - AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource)); - client.requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode); - } catch (NullPointerException e) { - Log.e(TAG, "Null ScoClient", e); - } - Binder.restoreCallingIdentity(ident); - } - - // @GuardedBy("AudioDeviceBroker.mSetModeLock") - @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ synchronized void stopBluetoothScoForClient(IBinder cb, - @NonNull String eventSource) { - ScoClient client = getScoClient(cb, false); - // The calling identity must be cleared before calling ScoClient.decCount(). - // decCount() calls requestScoState() which in turn can call BluetoothHeadset APIs - // and this must be done on behalf of system server to make sure permissions are granted. - final long ident = Binder.clearCallingIdentity(); - if (client != null) { - stopAndRemoveClient(client, eventSource); - } - Binder.restoreCallingIdentity(ident); - } - - // @GuardedBy("AudioDeviceBroker.mSetModeLock") - @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ synchronized void stopBluetoothScoForPid(int pid) { - ScoClient client = getScoClientForPid(pid); - if (client == null) { - return; - } - final String eventSource = new StringBuilder("stopBluetoothScoForPid(") - .append(pid).append(")").toString(); - stopAndRemoveClient(client, eventSource); - } - - @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - // @GuardedBy("BtHelper.this") - private void stopAndRemoveClient(ScoClient client, @NonNull String eventSource) { AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource)); - client.requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, - SCO_MODE_VIRTUAL_CALL); - // If a disconnection is pending, the client will be removed when clearAllScoClients() - // is called form receiveBtEvent() - if (mScoAudioState != SCO_STATE_DEACTIVATE_REQ - && mScoAudioState != SCO_STATE_DEACTIVATING) { - client.remove(false /*stop */, true /*unregister*/); - } + return requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode); + } + + // @GuardedBy("AudioDeviceBroker.mSetModeLock") + @GuardedBy("AudioDeviceBroker.mDeviceStateLock") + /*package*/ synchronized boolean stopBluetoothSco(@NonNull String eventSource) { + AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource)); + return requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, SCO_MODE_VIRTUAL_CALL); } /*package*/ synchronized void setHearingAidVolume(int index, int streamType) { @@ -507,7 +433,6 @@ public class BtHelper { // @GuardedBy("AudioDeviceBroker.mSetModeLock") @GuardedBy("AudioDeviceBroker.mDeviceStateLock") /*package*/ synchronized void resetBluetoothSco() { - clearAllScoClients(0, false); mScoAudioState = SCO_STATE_INACTIVE; broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); AudioSystem.setParameters("A2dpSuspended=false"); @@ -606,46 +531,64 @@ public class BtHelper { mDeviceBroker.postBroadcastScoConnectionState(state); } - private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { - if (btDevice == null) { - return true; + @Nullable AudioDeviceAttributes getHeadsetAudioDevice() { + if (mBluetoothHeadsetDevice == null) { + return null; } + return btHeadsetDeviceToAudioDevice(mBluetoothHeadsetDevice); + } + + private AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) { String address = btDevice.getAddress(); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + address = ""; + } BluetoothClass btClass = btDevice.getBluetoothClass(); - int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; - int[] outDeviceTypes = { - AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, - AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET, - AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT - }; + int nativeType = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO; if (btClass != null) { switch (btClass.getDeviceClass()) { case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: - outDeviceTypes = new int[] { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET }; + nativeType = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET; break; case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: - outDeviceTypes = new int[] { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT }; + nativeType = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT; break; } } - if (!BluetoothAdapter.checkBluetoothAddress(address)) { - address = ""; + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, "btHeadsetDeviceToAudioDevice btDevice: " + btDevice + + " btClass: " + (btClass == null ? "Unknown" : btClass) + + " nativeType: " + nativeType + " address: " + address); } + return new AudioDeviceAttributes(nativeType, address); + } + + private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { + if (btDevice == null) { + return true; + } + int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; + AudioDeviceAttributes audioDevice = btHeadsetDeviceToAudioDevice(btDevice); String btDeviceName = getName(btDevice); boolean result = false; if (isActive) { - result |= mDeviceBroker.handleDeviceConnection( - isActive, outDeviceTypes[0], address, btDeviceName); + result |= mDeviceBroker.handleDeviceConnection(isActive, audioDevice.getInternalType(), + audioDevice.getAddress(), btDeviceName); } else { + int[] outDeviceTypes = { + AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, + AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET, + AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT + }; for (int outDeviceType : outDeviceTypes) { result |= mDeviceBroker.handleDeviceConnection( - isActive, outDeviceType, address, btDeviceName); + isActive, outDeviceType, audioDevice.getAddress(), btDeviceName); } } // handleDeviceConnection() && result to make sure the method get executed result = mDeviceBroker.handleDeviceConnection( - isActive, inDevice, address, btDeviceName) && result; + isActive, inDevice, audioDevice.getAddress(), btDeviceName) && result; return result; } @@ -733,195 +676,122 @@ public class BtHelper { }; //---------------------------------------------------------------------- + // @GuardedBy("AudioDeviceBroker.mSetModeLock") - @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ synchronized void scoClientDied(Object obj) { - final ScoClient client = (ScoClient) obj; - client.remove(true /*stop*/, false /*unregister*/); - Log.w(TAG, "SCO client died"); - } - - private class ScoClient implements IBinder.DeathRecipient { - private IBinder mCb; // To be notified of client's death - private int mCreatorPid; - - ScoClient(IBinder cb) { - mCb = cb; - mCreatorPid = Binder.getCallingPid(); - } - - public void registerDeathRecipient() { - try { - mCb.linkToDeath(this, 0); - } catch (RemoteException e) { - Log.w(TAG, "ScoClient could not link to " + mCb + " binder death"); - } - } - - public void unregisterDeathRecipient() { - try { - mCb.unlinkToDeath(this, 0); - } catch (NoSuchElementException e) { - Log.w(TAG, "ScoClient could not not unregistered to binder"); - } - } - - @Override - public void binderDied() { - // process this from DeviceBroker's message queue to take the right locks since - // this event can impact SCO mode and requires querying audio mode stack - mDeviceBroker.postScoClientDied(this); - } - - IBinder getBinder() { - return mCb; - } - - int getPid() { - return mCreatorPid; - } - - // @GuardedBy("AudioDeviceBroker.mSetModeLock") - //@GuardedBy("AudioDeviceBroker.mDeviceStateLock") - @GuardedBy("BtHelper.this") - private boolean requestScoState(int state, int scoAudioMode) { - checkScoAudioState(); - if (mScoClients.size() != 1) { - Log.i(TAG, "requestScoState: state=" + state + ", scoAudioMode=" + scoAudioMode - + ", num SCO clients=" + mScoClients.size()); - return true; - } - if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { - // Make sure that the state transitions to CONNECTING even if we cannot initiate - // the connection. - broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING); - // Accept SCO audio activation only in NORMAL audio mode or if the mode is - // currently controlled by the same client process. - final int modeOwnerPid = mDeviceBroker.getModeOwnerPid(); - if (modeOwnerPid != 0 && (modeOwnerPid != mCreatorPid)) { - Log.w(TAG, "requestScoState: audio mode is not NORMAL and modeOwnerPid " - + modeOwnerPid + " != creatorPid " + mCreatorPid); + //@GuardedBy("AudioDeviceBroker.mDeviceStateLock") + @GuardedBy("BtHelper.this") + private boolean requestScoState(int state, int scoAudioMode) { + checkScoAudioState(); + if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { + // Make sure that the state transitions to CONNECTING even if we cannot initiate + // the connection. + broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING); + switch (mScoAudioState) { + case SCO_STATE_INACTIVE: + mScoAudioMode = scoAudioMode; + if (scoAudioMode == SCO_MODE_UNDEFINED) { + mScoAudioMode = SCO_MODE_VIRTUAL_CALL; + if (mBluetoothHeadsetDevice != null) { + mScoAudioMode = Settings.Global.getInt( + mDeviceBroker.getContentResolver(), + "bluetooth_sco_channel_" + + mBluetoothHeadsetDevice.getAddress(), + SCO_MODE_VIRTUAL_CALL); + if (mScoAudioMode > SCO_MODE_MAX || mScoAudioMode < 0) { + mScoAudioMode = SCO_MODE_VIRTUAL_CALL; + } + } + } + if (mBluetoothHeadset == null) { + if (getBluetoothHeadset()) { + mScoAudioState = SCO_STATE_ACTIVATE_REQ; + } else { + Log.w(TAG, "requestScoState: getBluetoothHeadset failed during" + + " connection, mScoAudioMode=" + mScoAudioMode); + broadcastScoConnectionState( + AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + return false; + } + break; + } + if (mBluetoothHeadsetDevice == null) { + Log.w(TAG, "requestScoState: no active device while connecting," + + " mScoAudioMode=" + mScoAudioMode); + broadcastScoConnectionState( + AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + return false; + } + if (connectBluetoothScoAudioHelper(mBluetoothHeadset, + mBluetoothHeadsetDevice, mScoAudioMode)) { + mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; + } else { + Log.w(TAG, "requestScoState: connect to " + + mBluetoothHeadsetDevice + + " failed, mScoAudioMode=" + mScoAudioMode); + broadcastScoConnectionState( + AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + return false; + } + break; + case SCO_STATE_DEACTIVATING: + mScoAudioState = SCO_STATE_ACTIVATE_REQ; + break; + case SCO_STATE_DEACTIVATE_REQ: + mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; + broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED); + break; + case SCO_STATE_ACTIVE_INTERNAL: + Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return"); + break; + default: + Log.w(TAG, "requestScoState: failed to connect in state " + + mScoAudioState + ", scoAudioMode=" + scoAudioMode); broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); return false; - } - switch (mScoAudioState) { - case SCO_STATE_INACTIVE: - mScoAudioMode = scoAudioMode; - if (scoAudioMode == SCO_MODE_UNDEFINED) { - mScoAudioMode = SCO_MODE_VIRTUAL_CALL; - if (mBluetoothHeadsetDevice != null) { - mScoAudioMode = Settings.Global.getInt( - mDeviceBroker.getContentResolver(), - "bluetooth_sco_channel_" - + mBluetoothHeadsetDevice.getAddress(), - SCO_MODE_VIRTUAL_CALL); - if (mScoAudioMode > SCO_MODE_MAX || mScoAudioMode < 0) { - mScoAudioMode = SCO_MODE_VIRTUAL_CALL; - } - } - } - if (mBluetoothHeadset == null) { - if (getBluetoothHeadset()) { - mScoAudioState = SCO_STATE_ACTIVATE_REQ; - } else { - Log.w(TAG, "requestScoState: getBluetoothHeadset failed during" - + " connection, mScoAudioMode=" + mScoAudioMode); - broadcastScoConnectionState( - AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - return false; - } - break; - } - if (mBluetoothHeadsetDevice == null) { - Log.w(TAG, "requestScoState: no active device while connecting," - + " mScoAudioMode=" + mScoAudioMode); - broadcastScoConnectionState( - AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - return false; - } - if (connectBluetoothScoAudioHelper(mBluetoothHeadset, - mBluetoothHeadsetDevice, mScoAudioMode)) { - mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; + } + } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { + switch (mScoAudioState) { + case SCO_STATE_ACTIVE_INTERNAL: + if (mBluetoothHeadset == null) { + if (getBluetoothHeadset()) { + mScoAudioState = SCO_STATE_DEACTIVATE_REQ; } else { - Log.w(TAG, "requestScoState: connect to " + mBluetoothHeadsetDevice - + " failed, mScoAudioMode=" + mScoAudioMode); + Log.w(TAG, "requestScoState: getBluetoothHeadset failed during" + + " disconnection, mScoAudioMode=" + mScoAudioMode); + mScoAudioState = SCO_STATE_INACTIVE; broadcastScoConnectionState( AudioManager.SCO_AUDIO_STATE_DISCONNECTED); return false; } break; - case SCO_STATE_DEACTIVATING: - mScoAudioState = SCO_STATE_ACTIVATE_REQ; - break; - case SCO_STATE_DEACTIVATE_REQ: - mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; - broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED); - break; - case SCO_STATE_ACTIVE_INTERNAL: - Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return"); - break; - default: - Log.w(TAG, "requestScoState: failed to connect in state " - + mScoAudioState + ", scoAudioMode=" + scoAudioMode); - broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - return false; - } - } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { - switch (mScoAudioState) { - case SCO_STATE_ACTIVE_INTERNAL: - if (mBluetoothHeadset == null) { - if (getBluetoothHeadset()) { - mScoAudioState = SCO_STATE_DEACTIVATE_REQ; - } else { - Log.w(TAG, "requestScoState: getBluetoothHeadset failed during" - + " disconnection, mScoAudioMode=" + mScoAudioMode); - mScoAudioState = SCO_STATE_INACTIVE; - broadcastScoConnectionState( - AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - return false; - } - break; - } - if (mBluetoothHeadsetDevice == null) { - mScoAudioState = SCO_STATE_INACTIVE; - broadcastScoConnectionState( - AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - break; - } - if (disconnectBluetoothScoAudioHelper(mBluetoothHeadset, - mBluetoothHeadsetDevice, mScoAudioMode)) { - mScoAudioState = SCO_STATE_DEACTIVATING; - } else { - mScoAudioState = SCO_STATE_INACTIVE; - broadcastScoConnectionState( - AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - } - break; - case SCO_STATE_ACTIVATE_REQ: + } + if (mBluetoothHeadsetDevice == null) { mScoAudioState = SCO_STATE_INACTIVE; - broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + broadcastScoConnectionState( + AudioManager.SCO_AUDIO_STATE_DISCONNECTED); break; - default: - Log.w(TAG, "requestScoState: failed to disconnect in state " - + mScoAudioState + ", scoAudioMode=" + scoAudioMode); - broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); - return false; - } + } + if (disconnectBluetoothScoAudioHelper(mBluetoothHeadset, + mBluetoothHeadsetDevice, mScoAudioMode)) { + mScoAudioState = SCO_STATE_DEACTIVATING; + } else { + mScoAudioState = SCO_STATE_INACTIVE; + broadcastScoConnectionState( + AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + } + break; + case SCO_STATE_ACTIVATE_REQ: + mScoAudioState = SCO_STATE_INACTIVE; + broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + break; + default: + Log.w(TAG, "requestScoState: failed to disconnect in state " + + mScoAudioState + ", scoAudioMode=" + scoAudioMode); + broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + return false; } - return true; - } - - @GuardedBy("BtHelper.this") - void remove(boolean stop, boolean unregister) { - if (unregister) { - unregisterDeathRecipient(); - } - if (stop) { - requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, - SCO_MODE_VIRTUAL_CALL); - } - mScoClients.remove(this); } + return true; } //----------------------------------------------------- @@ -974,49 +844,6 @@ public class BtHelper { } } - - @GuardedBy("BtHelper.this") - private ScoClient getScoClient(IBinder cb, boolean create) { - for (ScoClient existingClient : mScoClients) { - if (existingClient.getBinder() == cb) { - return existingClient; - } - } - if (create) { - ScoClient newClient = new ScoClient(cb); - newClient.registerDeathRecipient(); - mScoClients.add(newClient); - return newClient; - } - return null; - } - - @GuardedBy("BtHelper.this") - private ScoClient getScoClientForPid(int pid) { - for (ScoClient cl : mScoClients) { - if (cl.getPid() == pid) { - return cl; - } - } - return null; - } - - // @GuardedBy("AudioDeviceBroker.mSetModeLock") - //@GuardedBy("AudioDeviceBroker.mDeviceStateLock") - @GuardedBy("BtHelper.this") - private void clearAllScoClients(int exceptPid, boolean stopSco) { - final ArrayList clients = new ArrayList(); - for (ScoClient cl : mScoClients) { - if (cl.getPid() != exceptPid) { - clients.add(cl); - } - } - for (ScoClient cl : clients) { - cl.remove(stopSco, true /*unregister*/); - } - - } - private boolean getBluetoothHeadset() { boolean result = false; BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); @@ -1062,10 +889,6 @@ public class BtHelper { pw.println(prefix + "mBluetoothHeadsetDevice: " + mBluetoothHeadsetDevice); pw.println(prefix + "mScoAudioState: " + scoAudioStateToString(mScoAudioState)); pw.println(prefix + "mScoAudioMode: " + scoAudioModeToString(mScoAudioMode)); - pw.println(prefix + "Sco clients:"); - mScoClients.forEach((cl) -> { - pw.println(" " + prefix + "pid: " + cl.getPid() + " cb: " + cl.getBinder()); }); - pw.println("\n" + prefix + "mHearingAid: " + mHearingAid); pw.println(prefix + "mA2dp: " + mA2dp); pw.println(prefix + "mAvrcpAbsVolSupported: " + mAvrcpAbsVolSupported);