Loading android/app/src/com/android/bluetooth/btservice/AdapterService.java +259 −42 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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 Loading @@ -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; } } Loading Loading @@ -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 = { Loading Loading @@ -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; } } } Loading @@ -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-------- Loading android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java +71 −26 Original line number Diff line number Diff line Loading @@ -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; } Loading android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java +90 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
android/app/src/com/android/bluetooth/btservice/AdapterService.java +259 −42 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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 Loading @@ -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; } } Loading Loading @@ -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 = { Loading Loading @@ -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; } } } Loading @@ -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-------- Loading
android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java +71 −26 Original line number Diff line number Diff line Loading @@ -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; } Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java +90 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes