Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 120a65d0 authored by Rahul Sabnis's avatar Rahul Sabnis
Browse files

BT changes for preferred audio profile interactions with the audio

framework

Tag: #feature
Bug: 265077412
Test: atest DatabaseManagerTest
Change-Id: I5e2b2e2cd76feca010735cd08c749b5ccd82f17e
parent fe3c0d79
Loading
Loading
Loading
Loading
+259 −42
Original line number Diff line number Diff line
@@ -180,6 +180,7 @@ public class AdapterService extends Service {
    private static final int MIN_OFFLOADED_SCAN_STORAGE_BYTES = 1024;
    private static final Duration PENDING_SOCKET_HANDOFF_TIMEOUT = Duration.ofMinutes(1);
    private static final Duration GENERATE_LOCAL_OOB_DATA_TIMEOUT = Duration.ofSeconds(2);
    private static final Duration PREFERRED_AUDIO_PROFILE_CHANGE_TIMEOUT = Duration.ofSeconds(10);

    private final Object mEnergyInfoLock = new Object();
    private int mStackReportedState;
@@ -306,7 +307,9 @@ public class AdapterService extends Service {
            mPreferredAudioProfilesCallbacks;
    private RemoteCallbackList<IBluetoothQualityReportReadyCallback>
            mBluetoothQualityReportReadyCallbacks;
    private Set<BluetoothDevice> mDevicesPendingAudioProfileChanges = new HashSet<>();
    // Map<groupId, PendingAudioProfilePreferenceRequest>
    private final Map<Integer, PendingAudioProfilePreferenceRequest>
            mCsipGroupsPendingAudioProfileChanges = new HashMap<>();
    //Only BluetoothManagerService should be registered
    private RemoteCallbackList<IBluetoothCallback> mCallbacks;
    private int mCurrentRequestId;
@@ -408,6 +411,7 @@ public class AdapterService extends Service {
    private static final int MESSAGE_PROFILE_SERVICE_STATE_CHANGED = 1;
    private static final int MESSAGE_PROFILE_SERVICE_REGISTERED = 2;
    private static final int MESSAGE_PROFILE_SERVICE_UNREGISTERED = 3;
    private static final int MESSAGE_PREFERRED_AUDIO_PROFILES_AUDIO_FRAMEWORK_TIMEOUT = 4;

    class AdapterServiceHandler extends Handler {
        @Override
@@ -427,6 +431,21 @@ public class AdapterService extends Service {
                    verboseLog("handleMessage() - MESSAGE_PROFILE_SERVICE_UNREGISTERED");
                    unregisterProfileService((ProfileService) msg.obj);
                    break;
                case MESSAGE_PREFERRED_AUDIO_PROFILES_AUDIO_FRAMEWORK_TIMEOUT:
                    errorLog("handleMessage() - "
                            + "MESSAGE_PREFERRED_PROFILE_CHANGE_AUDIO_FRAMEWORK_TIMEOUT");
                    int groupId = (int) msg.obj;

                    synchronized (mCsipGroupsPendingAudioProfileChanges) {
                        removeFromPendingAudioProfileChanges(groupId);
                        PendingAudioProfilePreferenceRequest request =
                                mCsipGroupsPendingAudioProfileChanges.remove(groupId);
                        Log.e(TAG, "Preferred audio profiles change audio framework timeout for "
                                + "device " + request.mDeviceRequested);
                        sendPreferredAudioProfilesCallbackToApps(request.mDeviceRequested,
                                request.mRequestedPreferences, BluetoothStatusCodes.ERROR_TIMEOUT);
                    }
                    break;
            }
        }

@@ -503,6 +522,34 @@ public class AdapterService extends Service {

    private final AdapterServiceHandler mHandler = new AdapterServiceHandler();

    /**
     * Stores information about requests made to the audio framework arising from calls to
     * {@link BluetoothAdapter#setPreferredAudioProfiles(BluetoothDevice, Bundle)}.
     */
    private static class PendingAudioProfilePreferenceRequest {
        // The newly requested preferences
        final Bundle mRequestedPreferences;
        // Reference counter for how many calls are pending completion in the audio framework
        int mRemainingRequestsToAudioFramework;
        // The device with which the request was made. Used for sending the callback.
        final BluetoothDevice mDeviceRequested;

        /**
         * Constructs an entity to store information about pending preferred audio profile changes.
         *
         * @param preferences newly requested preferences
         * @param numRequestsToAudioFramework how many active device changed requests are sent to
         *                                    the audio framework
         * @param device the device with which the request was made
         */
        PendingAudioProfilePreferenceRequest(Bundle preferences,
                int numRequestsToAudioFramework, BluetoothDevice device) {
            mRequestedPreferences = preferences;
            mRemainingRequestsToAudioFramework = numRequestsToAudioFramework;
            mDeviceRequested = device;
        }
    }

    @Override
    @RequiresPermission(
            allOf = {
@@ -4868,33 +4915,164 @@ public class AdapterService extends Service {
            Log.e(TAG, "setPreferredAudioProfiles: Not a dual mode audio device");
            return BluetoothStatusCodes.ERROR_NOT_DUAL_MODE_AUDIO_DEVICE;
        }
        // Gets the lead device in the CSIP group to set the preference
        BluetoothDevice groupLead = mLeAudioService.getLeadDevice(device);
        if (groupLead == null) {
        // Checks if the device is part of an LE Audio group
        int groupId = mLeAudioService.getGroupId(device);
        List<BluetoothDevice> groupDevices = mLeAudioService.getGroupDevices(groupId);
        if (groupDevices.isEmpty()) {
            return BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED;
        }

        synchronized (mDevicesPendingAudioProfileChanges) {
            if (mDevicesPendingAudioProfileChanges.contains(groupLead)) {
                return BluetoothStatusCodes.ERROR_ANOTHER_ACTIVE_REQUEST;
            }

        // Copies relevant keys & values from modeToProfile bundle
        Bundle strippedPreferences = new Bundle();
        if (modeToProfileBundle.containsKey(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY)
                    && isOutputOnlyAudioSupported(mLeAudioService.getGroupDevices(device))) {
                strippedPreferences.putInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY,
                        modeToProfileBundle.getInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY));
                && isOutputOnlyAudioSupported(groupDevices)) {
            int outputOnlyProfile = modeToProfileBundle.getInt(
                    BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
            if (outputOnlyProfile != BluetoothProfile.A2DP
                    && outputOnlyProfile != BluetoothProfile.LE_AUDIO) {
                throw new IllegalArgumentException("AUDIO_MODE_OUTPUT_ONLY has invalid value: "
                        + outputOnlyProfile);
            }
            strippedPreferences.putInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY, outputOnlyProfile);
        }
        if (modeToProfileBundle.containsKey(BluetoothAdapter.AUDIO_MODE_DUPLEX)
                    && isDuplexAudioSupported(mLeAudioService.getGroupDevices(device))) {
                strippedPreferences.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX,
                        modeToProfileBundle.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
                && isDuplexAudioSupported(groupDevices)) {
            int duplexProfile = modeToProfileBundle.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
            if (duplexProfile != BluetoothProfile.HEADSET
                    && duplexProfile != BluetoothProfile.LE_AUDIO) {
                throw new IllegalArgumentException("AUDIO_MODE_DUPLEX has invalid value: "
                        + duplexProfile);
            }
            strippedPreferences.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, duplexProfile);
        }

        synchronized (mCsipGroupsPendingAudioProfileChanges) {
            if (mCsipGroupsPendingAudioProfileChanges.containsKey(groupId)) {
                return BluetoothStatusCodes.ERROR_ANOTHER_ACTIVE_REQUEST;
            }

            mDevicesPendingAudioProfileChanges.add(groupLead);
            return mDatabaseManager.setPreferredAudioProfiles(groupLead,
            Bundle previousPreferences = getPreferredAudioProfiles(device);

            int dbResult = mDatabaseManager.setPreferredAudioProfiles(groupDevices,
                    strippedPreferences);
            if (dbResult != BluetoothStatusCodes.SUCCESS) {
                return dbResult;
            }

            /* Populates the HashMap to hold requests on the groupId. We will update
            numRequestsToAudioFramework after we make requests to the audio framework */
            PendingAudioProfilePreferenceRequest holdRequest =
                    new PendingAudioProfilePreferenceRequest(strippedPreferences, 0, device);
            mCsipGroupsPendingAudioProfileChanges.put(groupId, holdRequest);

            // Notifies audio framework via the handler thread to avoid this blocking calls
            mHandler.post(() -> sendPreferredAudioProfileChangeToAudioFramework(
                    device, strippedPreferences, previousPreferences));
            return BluetoothStatusCodes.SUCCESS;
        }
    }

    /**
     * Sends the updated preferred audio profiles to the audio framework.
     *
     * @param device is the device with updated audio preferences
     * @param strippedPreferences is a {@link Bundle} containing the preferences
     */
    private void sendPreferredAudioProfileChangeToAudioFramework(BluetoothDevice device,
            Bundle strippedPreferences, Bundle previousPreferences) {
        int newOutput = strippedPreferences.getInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
        int newDuplex = strippedPreferences.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
        int previousOutput = previousPreferences.getInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
        int previousDuplex = previousPreferences.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);

        Log.i(TAG, "sendPreferredAudioProfileChangeToAudioFramework: changing output from "
                + BluetoothProfile.getProfileName(previousOutput) + " to "
                + BluetoothProfile.getProfileName(newOutput) + " and duplex from "
                + BluetoothProfile.getProfileName(previousDuplex) + " to "
                + BluetoothProfile.getProfileName(newDuplex));

        // If no change from existing preferences, do not inform audio framework
        if (previousOutput == newOutput && previousDuplex == newDuplex) {
            Log.i(TAG, "No change to preferred audio profiles, no requests to Audio FW");
            sendPreferredAudioProfilesCallbackToApps(device, strippedPreferences,
                    BluetoothStatusCodes.SUCCESS);
            return;
        }

        int numRequestsToAudioFw = 0;

        // Checks if the device is part of an LE Audio group
        int groupId = mLeAudioService.getGroupId(device);
        List<BluetoothDevice> groupDevices = mLeAudioService.getGroupDevices(groupId);
        if (groupDevices.isEmpty()) {
            Log.i(TAG, "sendPreferredAudioProfileChangeToAudioFramework: Empty LEA group for "
                    + "device - " + device);
            sendPreferredAudioProfilesCallbackToApps(device, strippedPreferences,
                    BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED);
            return;
        }

        synchronized (mCsipGroupsPendingAudioProfileChanges) {

            if (previousOutput != newOutput) {
                if (newOutput == BluetoothProfile.A2DP && mA2dpService.getActiveDevice() != null
                        && groupDevices.contains(mA2dpService.getActiveDevice())) {
                    Log.i(TAG, "Sent change for AUDIO_MODE_OUTPUT_ONLY to A2DP to Audio FW");
                    numRequestsToAudioFw +=
                            mA2dpService.sendPreferredAudioProfileChangeToAudioFramework();
                } else if (newOutput == BluetoothProfile.LE_AUDIO
                        && mLeAudioService.getActiveGroupId() == groupId) {
                    Log.i(TAG, "Sent change for AUDIO_MODE_OUTPUT_ONLY to LE_AUDIO to Audio FW");
                    numRequestsToAudioFw +=
                            mLeAudioService.sendPreferredAudioProfileChangeToAudioFramework();
                }
            }

            if (previousDuplex != newDuplex) {
                if (newDuplex == BluetoothProfile.HEADSET
                        && mHeadsetService.getActiveDevice() != null
                        && groupDevices.contains(mHeadsetService.getActiveDevice())) {
                    Log.i(TAG, "Sent change for AUDIO_MODE_DUPLEX to HFP to Audio FW");
                    // TODO(b/275426145): Add similar HFP method in BluetoothProfileConnectionInfo
                    numRequestsToAudioFw +=
                            mA2dpService.sendPreferredAudioProfileChangeToAudioFramework();
                } else if (newDuplex == BluetoothProfile.LE_AUDIO
                        && mLeAudioService.getActiveGroupId() == groupId) {
                    Log.i(TAG, "Sent change for AUDIO_MODE_DUPLEX to LE_AUDIO to Audio FW");
                    numRequestsToAudioFw +=
                            mLeAudioService.sendPreferredAudioProfileChangeToAudioFramework();
                }
            }

            Log.i(TAG,
                    "sendPreferredAudioProfileChangeToAudioFramework: sent " + numRequestsToAudioFw
                            + " request(s) to the Audio Framework for device: " + device);

            if (numRequestsToAudioFw > 0) {
                mCsipGroupsPendingAudioProfileChanges.put(groupId,
                        new PendingAudioProfilePreferenceRequest(strippedPreferences,
                                numRequestsToAudioFw, device));

                Message m = mHandler.obtainMessage(
                        MESSAGE_PREFERRED_AUDIO_PROFILES_AUDIO_FRAMEWORK_TIMEOUT);
                m.obj = groupId;
                mHandler.sendMessageDelayed(m, PREFERRED_AUDIO_PROFILE_CHANGE_TIMEOUT.toMillis());
                return;
            }
        }
        sendPreferredAudioProfilesCallbackToApps(device, strippedPreferences,
                BluetoothStatusCodes.SUCCESS);
    }

    private void removeFromPendingAudioProfileChanges(int groupId) {
        synchronized (mCsipGroupsPendingAudioProfileChanges) {
            Log.i(TAG, "removeFromPendingAudioProfileChanges: Timeout on change for groupId="
                    + groupId);
            if (!mCsipGroupsPendingAudioProfileChanges.containsKey(groupId)) {
                Log.e(TAG, "removeFromPendingAudioProfileChanges( " + groupId + ", " + groupId
                        + ") is not pending");
                return;
            }
        }
    }

@@ -4906,42 +5084,81 @@ public class AdapterService extends Service {
     * @param device the remote device whose preferred audio profiles have been changed
     * @return whether the Bluetooth stack acknowledged the change successfully
     */

    private int notifyActiveDeviceChangeApplied(BluetoothDevice device) {
        // Gets the lead device in the CSIP group to set the preference
        BluetoothDevice groupLead = mLeAudioService.getLeadDevice(device);
        if (groupLead == null) {
        if (mLeAudioService == null) {
            Log.e(TAG, "LE Audio profile not enabled");
            return BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED;
        }

        int groupId = mLeAudioService.getGroupId(device);
        if (groupId == LE_AUDIO_GROUP_ID_INVALID) {
            return BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED;
        }

        synchronized (mDevicesPendingAudioProfileChanges) {
            if (!mDevicesPendingAudioProfileChanges.contains(groupLead)) {
        synchronized (mCsipGroupsPendingAudioProfileChanges) {
            if (!mCsipGroupsPendingAudioProfileChanges.containsKey(groupId)) {
                Log.e(TAG, "notifyActiveDeviceChangeApplied, but no pending request for "
                        + "device: " + groupLead);
                        + "groupId: " + groupId);
                return BluetoothStatusCodes.ERROR_UNKNOWN;
            }

            if (mPreferredAudioProfilesCallbacks != null) {
            PendingAudioProfilePreferenceRequest pendingRequest =
                    mCsipGroupsPendingAudioProfileChanges.get(groupId);

            // If this is the final audio framework request, send callback to apps
            if (pendingRequest.mRemainingRequestsToAudioFramework == 1) {
                Log.i(TAG, "notifyActiveDeviceChangeApplied: Complete for device "
                        + pendingRequest.mDeviceRequested);
                sendPreferredAudioProfilesCallbackToApps(pendingRequest.mDeviceRequested,
                        pendingRequest.mRequestedPreferences, BluetoothStatusCodes.SUCCESS);
                // Removes the timeout from the handler
                mHandler.removeMessages(
                        MESSAGE_PREFERRED_AUDIO_PROFILES_AUDIO_FRAMEWORK_TIMEOUT, groupId);
            } else if (pendingRequest.mRemainingRequestsToAudioFramework > 1) {
                PendingAudioProfilePreferenceRequest updatedPendingRequest =
                        new PendingAudioProfilePreferenceRequest(
                                pendingRequest.mRequestedPreferences,
                                pendingRequest.mRemainingRequestsToAudioFramework - 1,
                                pendingRequest.mDeviceRequested);
                Log.i(TAG, "notifyActiveDeviceChangeApplied: Updating device "
                        + updatedPendingRequest.mDeviceRequested
                        + " with new remaining requests count="
                        + updatedPendingRequest.mRemainingRequestsToAudioFramework);
                mCsipGroupsPendingAudioProfileChanges.put(groupId, updatedPendingRequest);
            } else {
                Log.i(TAG, "notifyActiveDeviceChangeApplied: " + pendingRequest.mDeviceRequested
                        + " has no remaining requests to audio framework, but is still present in"
                        + " mCsipGroupsPendingAudioProfileChanges");
            }
        }

        return BluetoothStatusCodes.SUCCESS;
    }

    private void sendPreferredAudioProfilesCallbackToApps(BluetoothDevice device,
            Bundle preferredAudioProfiles, int status) {
        if (mPreferredAudioProfilesCallbacks == null) {
            return;
        }

        int n = mPreferredAudioProfilesCallbacks.beginBroadcast();
                debugLog("notifyActiveDeviceChangeApplied() - Broadcasting audio profile "
                        + "change applied to device: " + groupLead + " to " + n + " receivers.");
        debugLog("sendPreferredAudioProfilesCallbackToApps() - Broadcasting audio profile "
                + "change callback to device: " + device + " and status=" + status + " to " + n
                + " receivers.");
        for (int i = 0; i < n; i++) {
            try {
                mPreferredAudioProfilesCallbacks.getBroadcastItem(i)
                        .onPreferredAudioProfilesChanged(device,
                                        getPreferredAudioProfiles(device),
                                        BluetoothStatusCodes.SUCCESS);
                                preferredAudioProfiles,
                                status);
            } catch (RemoteException e) {
                        debugLog("notifyActiveDeviceChangeApplied() - Callback #" + i
                debugLog("sendPreferredAudioProfilesCallbackToApps() - Callback #" + i
                        + " failed (" + e + ")");
            }
        }
        mPreferredAudioProfilesCallbacks.finishBroadcast();
    }
            mDevicesPendingAudioProfileChanges.remove(groupLead);
        }

        return BluetoothStatusCodes.SUCCESS;
    }

    // ----API Methods--------

+71 −26
Original line number Diff line number Diff line
@@ -778,41 +778,86 @@ public class DatabaseManager {
     * Sets the preferred profile for the supplied audio modes. See
     * {@link BluetoothAdapter#setPreferredAudioProfiles(BluetoothDevice, Bundle)} for more details.
     *
     * @param device is the remote device for which we are setting the preferred audio profiles
     * If a device in the group has been designated to store the preference for the group, this will
     * update its database preferences. If there is not one designated, the first device from the
     * group list will be chosen for this purpose. From then on, any preferred audio profile changes
     * for this group will be stored on that device.
     *
     * @param groupDevices is the CSIP group for which we are setting the preferred audio profiles
     * @param modeToProfileBundle contains the preferred profile
     * @return
     * @return whether the new preferences were saved in the database
     */
    public int setPreferredAudioProfiles(BluetoothDevice device, Bundle modeToProfileBundle) {
    public int setPreferredAudioProfiles(List<BluetoothDevice> groupDevices,
            Bundle modeToProfileBundle) {
        Objects.requireNonNull(groupDevices, "groupDevices must not be null");
        Objects.requireNonNull(modeToProfileBundle, "modeToProfileBundle must not be null");
        if (groupDevices.isEmpty()) {
            throw new IllegalArgumentException("groupDevices cannot be empty");
        }
        int outputProfile = modeToProfileBundle.getInt(
                BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
        int duplexProfile = modeToProfileBundle.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
        boolean isPreferenceSet = false;

        synchronized (mMetadataCache) {
            for (BluetoothDevice device: groupDevices) {
                if (device == null) {
                    Log.e(TAG, "setPreferredAudioProfiles: device is null");
                    throw new IllegalArgumentException("setPreferredAudioProfiles: device is null");
                }

                String address = device.getAddress();

                if (!mMetadataCache.containsKey(address)) {
                    Log.e(TAG, "setPreferredAudioProfiles: Device not found in the database");
                    return BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED;
                }

            // Updates preferred audio profiles for the device
                // Finds the device in the group which stores the group's preferences
                Metadata metadata = mMetadataCache.get(address);
            int outputProfile = modeToProfileBundle.getInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
            int duplexProfile = modeToProfileBundle.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
                if (outputProfile != 0 && (metadata.preferred_output_only_profile != 0
                        || metadata.preferred_duplex_profile != 0)) {
                    Log.i(TAG, "setPreferredAudioProfiles: Updating OUTPUT_ONLY audio profile for "
                            + "device: " + device + " to "
                            + BluetoothProfile.getProfileName(outputProfile));
                    metadata.preferred_output_only_profile = outputProfile;
                    isPreferenceSet = true;
                }
                if (duplexProfile != 0 && (metadata.preferred_output_only_profile != 0
                        || metadata.preferred_duplex_profile != 0)) {
                    Log.i(TAG,
                            "setPreferredAudioProfiles: Updating DUPLEX audio profile for device: "
                                    + device + " to " + BluetoothProfile.getProfileName(
                                    duplexProfile));
                    metadata.preferred_duplex_profile = duplexProfile;
                    isPreferenceSet = true;
                }

                updateDatabase(metadata);
            }

            // If no device in the group has a preference set, choose the first device in the list
            if (!isPreferenceSet) {
                Log.i(TAG, "No device in the group has preferred audio profiles set");
                BluetoothDevice firstGroupDevice = groupDevices.get(0);
                // Updates preferred audio profiles for the device
                Metadata metadata = mMetadataCache.get(firstGroupDevice.getAddress());
                if (outputProfile != 0) {
                    Log.i(TAG, "setPreferredAudioProfiles: Updating output only audio profile for "
                        + "device: " + device + " to "
                            + "device: " + firstGroupDevice + " to "
                            + BluetoothProfile.getProfileName(outputProfile));
                    metadata.preferred_output_only_profile = outputProfile;
                }
                if (duplexProfile != 0) {
                Log.i(TAG, "setPreferredAudioProfiles: Updating duplex audio profile for device: "
                        + device + " to " + BluetoothProfile.getProfileName(duplexProfile));
                    Log.i(TAG,
                            "setPreferredAudioProfiles: Updating duplex audio profile for device: "
                                    + firstGroupDevice + " to " + BluetoothProfile.getProfileName(
                                    duplexProfile));
                    metadata.preferred_duplex_profile = duplexProfile;
                }

                updateDatabase(metadata);
            }
        }
        return BluetoothStatusCodes.SUCCESS;
    }

+90 −0

File changed.

Preview size limit exceeded, changes collapsed.