Loading android/app/src/com/android/bluetooth/btservice/AudioRoutingManager.java +252 −189 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; Loading Loading @@ -124,6 +125,28 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } /** * Requests to activate a specific profile for the given device. * * @param device The device to be activated. * @param profile The profile to be activated */ public void activateDeviceProfile(BluetoothDevice device, int profile) { mHandler.post( () -> mHandler.activateDeviceProfile( mHandler.getAudioRoutingDevice(device), profile)); } /** * Requests to remove active device for a specific profile. * * @param profile The profile to be deactivated */ public void removeActiveDevice(int profile, boolean hasFallbackDevice) { mHandler.post(() -> mHandler.removeActiveDevice(profile, hasFallbackDevice)); } /** * Called when active state of audio profiles changed * Loading Loading @@ -682,32 +705,9 @@ public class AudioRoutingManager extends ActiveDeviceManager { return fallbackCandidates; } @VisibleForTesting BluetoothDevice getA2dpActiveDevice() { synchronized (mLock) { return mA2dpActiveDevice; } } @VisibleForTesting BluetoothDevice getHfpActiveDevice() { synchronized (mLock) { return mHfpActiveDevice; } } @VisibleForTesting Set<BluetoothDevice> getHearingAidActiveDevices() { synchronized (mLock) { return mHearingAidActiveDevices; } } @VisibleForTesting BluetoothDevice getLeAudioActiveDevice() { synchronized (mLock) { return mLeAudioActiveDevice; } List<BluetoothDevice> getActiveDevices(int profile) { List<BluetoothDevice> devices = mHandler.mActiveDevices.get(profile); return devices == null ? Collections.emptyList() : devices; } @GuardedBy("mLock") Loading Loading @@ -811,11 +811,12 @@ public class AudioRoutingManager extends ActiveDeviceManager { Log.d(TAG, "Can not activate now: " + BluetoothProfile.getProfileName(profile)); } mHandler.postDelayed( () -> arDevice.activateProfile(profile), () -> activateDeviceProfile(arDevice, profile), arDevice, A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); return; } arDevice.activateProfile(profile); activateDeviceProfile(arDevice, profile); } public void handleProfileDisconnected(int profile, BluetoothDevice device) { Loading Loading @@ -847,7 +848,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { activeDevices.remove(device); if (activeDevices.size() == 0) { if (!setFallbackDeviceActive()) { arDevice.deactivate(profile, false); removeActiveDevice(profile, false); } } } Loading Loading @@ -878,7 +879,24 @@ public class AudioRoutingManager extends ActiveDeviceManager { getAudioRoutingDevice( mDbManager.getMostRecentlyConnectedDevicesInList(candidates)); if (deviceToActivate != null) { return deviceToActivate.activateDevice(); if (DBG) { Log.d(TAG, "activateDevice: device=" + deviceToActivate.device); } // Try to activate hearing aid and LE audio first if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.HEARING_AID)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.HEARING_AID); } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.LE_AUDIO); } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.A2DP)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.A2DP); } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.HEADSET)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.HEADSET); } Log.w( TAG, "Fail to activate the device: " + deviceToActivate.device + ", no connected audio profiles"); } return false; } Loading Loading @@ -922,68 +940,33 @@ public class AudioRoutingManager extends ActiveDeviceManager { return arDevice; } // TODO: make AudioRoutingDevice private public class AudioRoutingDevice { public BluetoothDevice device; public Set<Integer> supportedProfiles; public Set<Integer> connectedProfiles; public boolean canActivateNow(int profile) { if (!connectedProfiles.contains(profile)) return false; // TODO: Return false if there are another active remote streaming an audio. // TODO: consider LE audio and HearingAid. return switch (profile) { case BluetoothProfile.HEADSET -> !supportedProfiles.contains( BluetoothProfile.A2DP) || connectedProfiles.contains(BluetoothProfile.A2DP); case BluetoothProfile.A2DP -> !supportedProfiles.contains( BluetoothProfile.HEADSET) || connectedProfiles.contains(BluetoothProfile.HEADSET); default -> true; }; } /** * Activate the device. If supported, this will activate hearing aid and LE audio first, * then A2DP and HFP. * * @return true if any profile was activated. */ public boolean activateDevice() { if (DBG) { Log.d(TAG, "activateDevice: device=" + device); } // Try to activate hearing aid and LE audio first if (connectedProfiles.contains(BluetoothProfile.HEARING_AID)) { return activateProfile(BluetoothProfile.HEARING_AID); } else if (connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { return activateProfile(BluetoothProfile.LE_AUDIO); } else if (connectedProfiles.contains(BluetoothProfile.A2DP)) { return activateProfile(BluetoothProfile.A2DP); } else if (connectedProfiles.contains(BluetoothProfile.HEADSET)) { return activateProfile(BluetoothProfile.HEADSET); } Log.w( TAG, "Fail to activate the device: " + device + ", no connected audio profiles"); return false; } /** * Activate the given profile and related profiles if possible. A2DP and HFP would be * activated together if possible. If there are any activated profiles that can't be * activated together, they will be deactivated. * * @param arDevice the device of which one or more profiles to be activated * @param profile the profile requited to be activated * @return true if any profile was activated or the given profile was already active. */ @SuppressLint("MissingPermission") public boolean activateProfile(int profile) { public boolean activateDeviceProfile(@NonNull AudioRoutingDevice arDevice, int profile) { mHandler.removeCallbacksAndMessages(arDevice); if (DBG) { Log.d( TAG, "activateDeviceProfile(" + arDevice.device + ", " + BluetoothProfile.getProfileName(profile) + ")"); } List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices != null && activeDevices.contains(device)) { if (activeDevices != null && activeDevices.contains(arDevice.device)) { return true; } HashSet<Integer> profilesToActivate = new HashSet<>(); HashSet<Integer> profilesToDeactivate = new HashSet<>(); for (int i = 0; i < mActiveDevices.size(); i++) { Loading @@ -993,100 +976,151 @@ public class AudioRoutingManager extends ActiveDeviceManager { profilesToActivate.add(profile); profilesToDeactivate.remove(profile); boolean checkLeAudioActive; switch (profile) { case BluetoothProfile.A2DP: profilesToDeactivate.remove(BluetoothProfile.HEADSET); if (connectedProfiles.contains(BluetoothProfile.HEADSET)) { activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); if (activeDevices == null || !activeDevices.contains(device)) { checkLeAudioActive = !arDevice.supportedProfiles.contains(BluetoothProfile.HEADSET); if (arDevice.connectedProfiles.contains(BluetoothProfile.HEADSET)) { profilesToActivate.add(BluetoothProfile.HEADSET); checkLeAudioActive = true; } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && activeDevices.contains(device)) { if (checkLeAudioActive && Utils.isDualModeAudioEnabled() && arDevice.connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { profilesToActivate.add(BluetoothProfile.LE_AUDIO); profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); } } break; case BluetoothProfile.HEADSET: profilesToDeactivate.remove(BluetoothProfile.A2DP); if (connectedProfiles.contains(BluetoothProfile.A2DP)) { activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); if (activeDevices == null || !activeDevices.contains(device)) { checkLeAudioActive = !arDevice.supportedProfiles.contains(BluetoothProfile.A2DP); if (arDevice.connectedProfiles.contains(BluetoothProfile.A2DP)) { profilesToActivate.add(BluetoothProfile.A2DP); checkLeAudioActive = true; } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && activeDevices.contains(device)) { if (checkLeAudioActive && Utils.isDualModeAudioEnabled() && arDevice.connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { profilesToActivate.add(BluetoothProfile.LE_AUDIO); profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); } } break; case BluetoothProfile.LE_AUDIO: if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); if (activeDevices != null && activeDevices.contains(device)) { profilesToDeactivate.remove(BluetoothProfile.HEADSET); } activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); if (activeDevices != null && activeDevices.contains(device)) { if (arDevice.connectedProfiles.contains(BluetoothProfile.A2DP)) { profilesToActivate.add(BluetoothProfile.A2DP); profilesToDeactivate.remove(BluetoothProfile.A2DP); } if (arDevice.connectedProfiles.contains(BluetoothProfile.HEADSET)) { profilesToActivate.add(BluetoothProfile.HEADSET); profilesToDeactivate.remove(BluetoothProfile.HEADSET); } } break; } boolean isAnyProfileActivated = false; for (Integer p : profilesToActivate) { for (int p : profilesToActivate) { activeDevices = mActiveDevices.get(p); if (activeDevices == null || !activeDevices.contains(arDevice.device)) { isAnyProfileActivated |= setActiveDevice(p, arDevice.device); } else { isAnyProfileActivated = true; } } // Do not deactivate profiles if no profiles were activated. if (!isAnyProfileActivated) return false; if (profilesToActivate.contains(BluetoothProfile.LE_AUDIO) || profilesToActivate.contains(BluetoothProfile.HEARING_AID)) { // Deactivate activated profiles if it doesn't contain the arDevice. for (int i = 0; i < mActiveDevices.size(); i++) { if (!mActiveDevices.valueAt(i).contains(arDevice.device)) { profilesToDeactivate.add(mActiveDevices.keyAt(i)); } } } for (int p : profilesToDeactivate) { removeActiveDevice(p, true); } return true; } @SuppressLint("MissingPermission") private boolean setActiveDevice(int profile, BluetoothDevice device) { if (DBG) { Log.d(TAG, "Activate profile: " + BluetoothProfile.getProfileName(p)); Log.d( TAG, "setActiveDevice(" + BluetoothProfile.getProfileName(profile) + ", " + device + ")"); } boolean activated = switch (profile) { case BluetoothProfile.A2DP -> { A2dpService service = mFactory.getA2dpService(); yield service == null ? false : service.setActiveDevice(device); } case BluetoothProfile.HEADSET -> { HeadsetService service = mFactory.getHeadsetService(); yield service == null ? false : service.setActiveDevice(device); } case BluetoothProfile.LE_AUDIO -> { LeAudioService service = mFactory.getLeAudioService(); yield service == null ? false : service.setActiveDevice(device); } case BluetoothProfile.HEARING_AID -> { HearingAidService service = mFactory.getHearingAidService(); yield service == null ? false : service.setActiveDevice(device); } boolean activated = switch (p) { case BluetoothProfile.A2DP -> setA2dpActiveDevice(device); case BluetoothProfile.HEADSET -> setHfpActiveDevice(device); case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(device); case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(device); default -> false; }; if (activated) { // TODO: handle this inside of setXxxActiveDevice() method activeDevices = mActiveDevices.get(p); List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices == null) { activeDevices = new ArrayList<>(); mActiveDevices.put(p, activeDevices); mActiveDevices.put(profile, activeDevices); } if (!canActivateTogether(p, device, activeDevices)) { if (!canActivateTogether(profile, device, activeDevices)) { activeDevices.clear(); } activeDevices.add(device); } isAnyProfileActivated |= activated; return activated; } // Do not deactivate profiles if no profiles were activated. if (!isAnyProfileActivated) return false; for (Integer p : profilesToDeactivate) { Log.d(TAG, "Deactivate profile: " + BluetoothProfile.getProfileName(p)); mActiveDevices.remove(p); switch (p) { case BluetoothProfile.A2DP -> setA2dpActiveDevice(null, true); case BluetoothProfile.HEADSET -> setHfpActiveDevice(null); case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(null, true); case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(null, true); private boolean removeActiveDevice(int profile, boolean hasFallbackDevice) { if (DBG) { Log.d( TAG, "removeActiveDevice(" + BluetoothProfile.getProfileName(profile) + ", hadFallbackDevice=" + hasFallbackDevice + ")"); } mActiveDevices.remove(profile); return switch (profile) { case BluetoothProfile.A2DP -> { A2dpService service = mFactory.getA2dpService(); yield service == null ? false : service.removeActiveDevice(!hasFallbackDevice); } case BluetoothProfile.HEADSET -> { HeadsetService service = mFactory.getHeadsetService(); yield service == null ? false : service.setActiveDevice(null); } return true; case BluetoothProfile.LE_AUDIO -> { LeAudioService service = mFactory.getLeAudioService(); yield service == null ? false : service.removeActiveDevice(hasFallbackDevice); } @SuppressLint("MissingPermission") public void deactivate(int profile, boolean hasFallbackDevice) { if (!mActiveDevices.contains(profile)) return; switch (profile) { case BluetoothProfile.A2DP -> setA2dpActiveDevice(null, hasFallbackDevice); case BluetoothProfile.HEADSET -> setHfpActiveDevice(null); case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(null, false); case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(null, false); case BluetoothProfile.HEARING_AID -> { HearingAidService service = mFactory.getHearingAidService(); yield service == null ? false : service.removeActiveDevice(!hasFallbackDevice); } mActiveDevices.remove(profile); default -> false; }; } private boolean canActivateTogether( Loading @@ -1095,7 +1129,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { return false; } switch (profile) { case BluetoothProfile.LE_AUDIO: { case BluetoothProfile.LE_AUDIO: { final LeAudioService leAudioService = mFactory.getLeAudioService(); if (leAudioService == null) { return false; Loading @@ -1107,7 +1142,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { } break; } case BluetoothProfile.HEARING_AID: { case BluetoothProfile.HEARING_AID: { final HearingAidService hearingAidService = mFactory.getHearingAidService(); if (hearingAidService == null) { return false; Loading @@ -1122,6 +1158,33 @@ public class AudioRoutingManager extends ActiveDeviceManager { } return false; } // TODO: make AudioRoutingDevice private public static class AudioRoutingDevice { public BluetoothDevice device; public Set<Integer> supportedProfiles; public Set<Integer> connectedProfiles; public boolean canActivateNow(int profile) { if (!connectedProfiles.contains(profile)) return false; // TODO: Return false if there are another active remote streaming an audio. return switch (profile) { case BluetoothProfile.HEADSET -> !supportedProfiles.contains( BluetoothProfile.A2DP) || connectedProfiles.contains(BluetoothProfile.A2DP); case BluetoothProfile.A2DP -> !supportedProfiles.contains( BluetoothProfile.HEADSET) || connectedProfiles.contains(BluetoothProfile.HEADSET); case BluetoothProfile.LE_AUDIO -> !Utils.isDualModeAudioEnabled() // Check all supported A2DP and HFP are connected if dual mode enabled || ((connectedProfiles.contains(BluetoothProfile.A2DP) || !supportedProfiles.contains(BluetoothProfile.A2DP)) && (connectedProfiles.contains(BluetoothProfile.HEADSET) || !supportedProfiles.contains( BluetoothProfile.HEADSET))); default -> true; }; } } } } android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +12 −5 Original line number Diff line number Diff line Loading @@ -1655,13 +1655,20 @@ public class LeAudioService extends ProfileService { return false; } if (!mFeatureFlags.audioRoutingCentralization()) { // If AUDIO_ROUTING_CENTRALIZATION, this will be checked inside AudioRoutingManager. if (Utils.isDualModeAudioEnabled()) { if (!mAdapterService.isAllSupportedClassicAudioProfilesActive(device)) { Log.e(TAG, "setActiveDevice(" + device + "): failed because the device is not " Log.e( TAG, "setActiveDevice(" + device + "): failed because the device is not " + "active for all supported classic audio profiles"); return false; } } } setActiveGroupWithDevice(device, false); return true; } Loading android/app/tests/unit/src/com/android/bluetooth/btservice/AudioRoutingManagerTest.java +207 −222 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
android/app/src/com/android/bluetooth/btservice/AudioRoutingManager.java +252 −189 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; Loading Loading @@ -124,6 +125,28 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } /** * Requests to activate a specific profile for the given device. * * @param device The device to be activated. * @param profile The profile to be activated */ public void activateDeviceProfile(BluetoothDevice device, int profile) { mHandler.post( () -> mHandler.activateDeviceProfile( mHandler.getAudioRoutingDevice(device), profile)); } /** * Requests to remove active device for a specific profile. * * @param profile The profile to be deactivated */ public void removeActiveDevice(int profile, boolean hasFallbackDevice) { mHandler.post(() -> mHandler.removeActiveDevice(profile, hasFallbackDevice)); } /** * Called when active state of audio profiles changed * Loading Loading @@ -682,32 +705,9 @@ public class AudioRoutingManager extends ActiveDeviceManager { return fallbackCandidates; } @VisibleForTesting BluetoothDevice getA2dpActiveDevice() { synchronized (mLock) { return mA2dpActiveDevice; } } @VisibleForTesting BluetoothDevice getHfpActiveDevice() { synchronized (mLock) { return mHfpActiveDevice; } } @VisibleForTesting Set<BluetoothDevice> getHearingAidActiveDevices() { synchronized (mLock) { return mHearingAidActiveDevices; } } @VisibleForTesting BluetoothDevice getLeAudioActiveDevice() { synchronized (mLock) { return mLeAudioActiveDevice; } List<BluetoothDevice> getActiveDevices(int profile) { List<BluetoothDevice> devices = mHandler.mActiveDevices.get(profile); return devices == null ? Collections.emptyList() : devices; } @GuardedBy("mLock") Loading Loading @@ -811,11 +811,12 @@ public class AudioRoutingManager extends ActiveDeviceManager { Log.d(TAG, "Can not activate now: " + BluetoothProfile.getProfileName(profile)); } mHandler.postDelayed( () -> arDevice.activateProfile(profile), () -> activateDeviceProfile(arDevice, profile), arDevice, A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); return; } arDevice.activateProfile(profile); activateDeviceProfile(arDevice, profile); } public void handleProfileDisconnected(int profile, BluetoothDevice device) { Loading Loading @@ -847,7 +848,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { activeDevices.remove(device); if (activeDevices.size() == 0) { if (!setFallbackDeviceActive()) { arDevice.deactivate(profile, false); removeActiveDevice(profile, false); } } } Loading Loading @@ -878,7 +879,24 @@ public class AudioRoutingManager extends ActiveDeviceManager { getAudioRoutingDevice( mDbManager.getMostRecentlyConnectedDevicesInList(candidates)); if (deviceToActivate != null) { return deviceToActivate.activateDevice(); if (DBG) { Log.d(TAG, "activateDevice: device=" + deviceToActivate.device); } // Try to activate hearing aid and LE audio first if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.HEARING_AID)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.HEARING_AID); } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.LE_AUDIO); } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.A2DP)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.A2DP); } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.HEADSET)) { return activateDeviceProfile(deviceToActivate, BluetoothProfile.HEADSET); } Log.w( TAG, "Fail to activate the device: " + deviceToActivate.device + ", no connected audio profiles"); } return false; } Loading Loading @@ -922,68 +940,33 @@ public class AudioRoutingManager extends ActiveDeviceManager { return arDevice; } // TODO: make AudioRoutingDevice private public class AudioRoutingDevice { public BluetoothDevice device; public Set<Integer> supportedProfiles; public Set<Integer> connectedProfiles; public boolean canActivateNow(int profile) { if (!connectedProfiles.contains(profile)) return false; // TODO: Return false if there are another active remote streaming an audio. // TODO: consider LE audio and HearingAid. return switch (profile) { case BluetoothProfile.HEADSET -> !supportedProfiles.contains( BluetoothProfile.A2DP) || connectedProfiles.contains(BluetoothProfile.A2DP); case BluetoothProfile.A2DP -> !supportedProfiles.contains( BluetoothProfile.HEADSET) || connectedProfiles.contains(BluetoothProfile.HEADSET); default -> true; }; } /** * Activate the device. If supported, this will activate hearing aid and LE audio first, * then A2DP and HFP. * * @return true if any profile was activated. */ public boolean activateDevice() { if (DBG) { Log.d(TAG, "activateDevice: device=" + device); } // Try to activate hearing aid and LE audio first if (connectedProfiles.contains(BluetoothProfile.HEARING_AID)) { return activateProfile(BluetoothProfile.HEARING_AID); } else if (connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { return activateProfile(BluetoothProfile.LE_AUDIO); } else if (connectedProfiles.contains(BluetoothProfile.A2DP)) { return activateProfile(BluetoothProfile.A2DP); } else if (connectedProfiles.contains(BluetoothProfile.HEADSET)) { return activateProfile(BluetoothProfile.HEADSET); } Log.w( TAG, "Fail to activate the device: " + device + ", no connected audio profiles"); return false; } /** * Activate the given profile and related profiles if possible. A2DP and HFP would be * activated together if possible. If there are any activated profiles that can't be * activated together, they will be deactivated. * * @param arDevice the device of which one or more profiles to be activated * @param profile the profile requited to be activated * @return true if any profile was activated or the given profile was already active. */ @SuppressLint("MissingPermission") public boolean activateProfile(int profile) { public boolean activateDeviceProfile(@NonNull AudioRoutingDevice arDevice, int profile) { mHandler.removeCallbacksAndMessages(arDevice); if (DBG) { Log.d( TAG, "activateDeviceProfile(" + arDevice.device + ", " + BluetoothProfile.getProfileName(profile) + ")"); } List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices != null && activeDevices.contains(device)) { if (activeDevices != null && activeDevices.contains(arDevice.device)) { return true; } HashSet<Integer> profilesToActivate = new HashSet<>(); HashSet<Integer> profilesToDeactivate = new HashSet<>(); for (int i = 0; i < mActiveDevices.size(); i++) { Loading @@ -993,100 +976,151 @@ public class AudioRoutingManager extends ActiveDeviceManager { profilesToActivate.add(profile); profilesToDeactivate.remove(profile); boolean checkLeAudioActive; switch (profile) { case BluetoothProfile.A2DP: profilesToDeactivate.remove(BluetoothProfile.HEADSET); if (connectedProfiles.contains(BluetoothProfile.HEADSET)) { activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); if (activeDevices == null || !activeDevices.contains(device)) { checkLeAudioActive = !arDevice.supportedProfiles.contains(BluetoothProfile.HEADSET); if (arDevice.connectedProfiles.contains(BluetoothProfile.HEADSET)) { profilesToActivate.add(BluetoothProfile.HEADSET); checkLeAudioActive = true; } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && activeDevices.contains(device)) { if (checkLeAudioActive && Utils.isDualModeAudioEnabled() && arDevice.connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { profilesToActivate.add(BluetoothProfile.LE_AUDIO); profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); } } break; case BluetoothProfile.HEADSET: profilesToDeactivate.remove(BluetoothProfile.A2DP); if (connectedProfiles.contains(BluetoothProfile.A2DP)) { activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); if (activeDevices == null || !activeDevices.contains(device)) { checkLeAudioActive = !arDevice.supportedProfiles.contains(BluetoothProfile.A2DP); if (arDevice.connectedProfiles.contains(BluetoothProfile.A2DP)) { profilesToActivate.add(BluetoothProfile.A2DP); checkLeAudioActive = true; } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && activeDevices.contains(device)) { if (checkLeAudioActive && Utils.isDualModeAudioEnabled() && arDevice.connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) { profilesToActivate.add(BluetoothProfile.LE_AUDIO); profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); } } break; case BluetoothProfile.LE_AUDIO: if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); if (activeDevices != null && activeDevices.contains(device)) { profilesToDeactivate.remove(BluetoothProfile.HEADSET); } activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); if (activeDevices != null && activeDevices.contains(device)) { if (arDevice.connectedProfiles.contains(BluetoothProfile.A2DP)) { profilesToActivate.add(BluetoothProfile.A2DP); profilesToDeactivate.remove(BluetoothProfile.A2DP); } if (arDevice.connectedProfiles.contains(BluetoothProfile.HEADSET)) { profilesToActivate.add(BluetoothProfile.HEADSET); profilesToDeactivate.remove(BluetoothProfile.HEADSET); } } break; } boolean isAnyProfileActivated = false; for (Integer p : profilesToActivate) { for (int p : profilesToActivate) { activeDevices = mActiveDevices.get(p); if (activeDevices == null || !activeDevices.contains(arDevice.device)) { isAnyProfileActivated |= setActiveDevice(p, arDevice.device); } else { isAnyProfileActivated = true; } } // Do not deactivate profiles if no profiles were activated. if (!isAnyProfileActivated) return false; if (profilesToActivate.contains(BluetoothProfile.LE_AUDIO) || profilesToActivate.contains(BluetoothProfile.HEARING_AID)) { // Deactivate activated profiles if it doesn't contain the arDevice. for (int i = 0; i < mActiveDevices.size(); i++) { if (!mActiveDevices.valueAt(i).contains(arDevice.device)) { profilesToDeactivate.add(mActiveDevices.keyAt(i)); } } } for (int p : profilesToDeactivate) { removeActiveDevice(p, true); } return true; } @SuppressLint("MissingPermission") private boolean setActiveDevice(int profile, BluetoothDevice device) { if (DBG) { Log.d(TAG, "Activate profile: " + BluetoothProfile.getProfileName(p)); Log.d( TAG, "setActiveDevice(" + BluetoothProfile.getProfileName(profile) + ", " + device + ")"); } boolean activated = switch (profile) { case BluetoothProfile.A2DP -> { A2dpService service = mFactory.getA2dpService(); yield service == null ? false : service.setActiveDevice(device); } case BluetoothProfile.HEADSET -> { HeadsetService service = mFactory.getHeadsetService(); yield service == null ? false : service.setActiveDevice(device); } case BluetoothProfile.LE_AUDIO -> { LeAudioService service = mFactory.getLeAudioService(); yield service == null ? false : service.setActiveDevice(device); } case BluetoothProfile.HEARING_AID -> { HearingAidService service = mFactory.getHearingAidService(); yield service == null ? false : service.setActiveDevice(device); } boolean activated = switch (p) { case BluetoothProfile.A2DP -> setA2dpActiveDevice(device); case BluetoothProfile.HEADSET -> setHfpActiveDevice(device); case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(device); case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(device); default -> false; }; if (activated) { // TODO: handle this inside of setXxxActiveDevice() method activeDevices = mActiveDevices.get(p); List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices == null) { activeDevices = new ArrayList<>(); mActiveDevices.put(p, activeDevices); mActiveDevices.put(profile, activeDevices); } if (!canActivateTogether(p, device, activeDevices)) { if (!canActivateTogether(profile, device, activeDevices)) { activeDevices.clear(); } activeDevices.add(device); } isAnyProfileActivated |= activated; return activated; } // Do not deactivate profiles if no profiles were activated. if (!isAnyProfileActivated) return false; for (Integer p : profilesToDeactivate) { Log.d(TAG, "Deactivate profile: " + BluetoothProfile.getProfileName(p)); mActiveDevices.remove(p); switch (p) { case BluetoothProfile.A2DP -> setA2dpActiveDevice(null, true); case BluetoothProfile.HEADSET -> setHfpActiveDevice(null); case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(null, true); case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(null, true); private boolean removeActiveDevice(int profile, boolean hasFallbackDevice) { if (DBG) { Log.d( TAG, "removeActiveDevice(" + BluetoothProfile.getProfileName(profile) + ", hadFallbackDevice=" + hasFallbackDevice + ")"); } mActiveDevices.remove(profile); return switch (profile) { case BluetoothProfile.A2DP -> { A2dpService service = mFactory.getA2dpService(); yield service == null ? false : service.removeActiveDevice(!hasFallbackDevice); } case BluetoothProfile.HEADSET -> { HeadsetService service = mFactory.getHeadsetService(); yield service == null ? false : service.setActiveDevice(null); } return true; case BluetoothProfile.LE_AUDIO -> { LeAudioService service = mFactory.getLeAudioService(); yield service == null ? false : service.removeActiveDevice(hasFallbackDevice); } @SuppressLint("MissingPermission") public void deactivate(int profile, boolean hasFallbackDevice) { if (!mActiveDevices.contains(profile)) return; switch (profile) { case BluetoothProfile.A2DP -> setA2dpActiveDevice(null, hasFallbackDevice); case BluetoothProfile.HEADSET -> setHfpActiveDevice(null); case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(null, false); case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(null, false); case BluetoothProfile.HEARING_AID -> { HearingAidService service = mFactory.getHearingAidService(); yield service == null ? false : service.removeActiveDevice(!hasFallbackDevice); } mActiveDevices.remove(profile); default -> false; }; } private boolean canActivateTogether( Loading @@ -1095,7 +1129,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { return false; } switch (profile) { case BluetoothProfile.LE_AUDIO: { case BluetoothProfile.LE_AUDIO: { final LeAudioService leAudioService = mFactory.getLeAudioService(); if (leAudioService == null) { return false; Loading @@ -1107,7 +1142,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { } break; } case BluetoothProfile.HEARING_AID: { case BluetoothProfile.HEARING_AID: { final HearingAidService hearingAidService = mFactory.getHearingAidService(); if (hearingAidService == null) { return false; Loading @@ -1122,6 +1158,33 @@ public class AudioRoutingManager extends ActiveDeviceManager { } return false; } // TODO: make AudioRoutingDevice private public static class AudioRoutingDevice { public BluetoothDevice device; public Set<Integer> supportedProfiles; public Set<Integer> connectedProfiles; public boolean canActivateNow(int profile) { if (!connectedProfiles.contains(profile)) return false; // TODO: Return false if there are another active remote streaming an audio. return switch (profile) { case BluetoothProfile.HEADSET -> !supportedProfiles.contains( BluetoothProfile.A2DP) || connectedProfiles.contains(BluetoothProfile.A2DP); case BluetoothProfile.A2DP -> !supportedProfiles.contains( BluetoothProfile.HEADSET) || connectedProfiles.contains(BluetoothProfile.HEADSET); case BluetoothProfile.LE_AUDIO -> !Utils.isDualModeAudioEnabled() // Check all supported A2DP and HFP are connected if dual mode enabled || ((connectedProfiles.contains(BluetoothProfile.A2DP) || !supportedProfiles.contains(BluetoothProfile.A2DP)) && (connectedProfiles.contains(BluetoothProfile.HEADSET) || !supportedProfiles.contains( BluetoothProfile.HEADSET))); default -> true; }; } } } }
android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +12 −5 Original line number Diff line number Diff line Loading @@ -1655,13 +1655,20 @@ public class LeAudioService extends ProfileService { return false; } if (!mFeatureFlags.audioRoutingCentralization()) { // If AUDIO_ROUTING_CENTRALIZATION, this will be checked inside AudioRoutingManager. if (Utils.isDualModeAudioEnabled()) { if (!mAdapterService.isAllSupportedClassicAudioProfilesActive(device)) { Log.e(TAG, "setActiveDevice(" + device + "): failed because the device is not " Log.e( TAG, "setActiveDevice(" + device + "): failed because the device is not " + "active for all supported classic audio profiles"); return false; } } } setActiveGroupWithDevice(device, false); return true; } Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/AudioRoutingManagerTest.java +207 −222 File changed.Preview size limit exceeded, changes collapsed. Show changes