1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.media; 18 19 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.bluetooth.BluetoothA2dp; 24 import android.bluetooth.BluetoothAdapter; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothHearingAid; 27 import android.bluetooth.BluetoothLeAudio; 28 import android.bluetooth.BluetoothProfile; 29 import android.content.BroadcastReceiver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.media.MediaRoute2Info; 34 import android.os.Handler; 35 import android.os.UserHandle; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.util.Slog; 39 import android.util.SparseBooleanArray; 40 41 import com.android.internal.R; 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.media.flags.Flags; 44 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Objects; 51 import java.util.Set; 52 import java.util.function.Function; 53 import java.util.stream.Collectors; 54 55 /** 56 * Maintains a list of connected {@link BluetoothDevice bluetooth devices} and allows their 57 * activation. 58 * 59 * <p>This class also serves as ground truth for assigning {@link MediaRoute2Info#getId() route ids} 60 * for bluetooth routes via {@link #getRouteIdForBluetoothAddress}. 61 */ 62 /* package */ class BluetoothDeviceRoutesManager { 63 private static final String TAG = SystemMediaRoute2Provider.TAG; 64 65 private static final String HEARING_AID_ROUTE_ID_PREFIX = "HEARING_AID_"; 66 private static final String LE_AUDIO_ROUTE_ID_PREFIX = "LE_AUDIO_"; 67 68 @NonNull 69 private final AdapterStateChangedReceiver mAdapterStateChangedReceiver = 70 new AdapterStateChangedReceiver(); 71 72 @NonNull 73 private final DeviceStateChangedReceiver mDeviceStateChangedReceiver = 74 new DeviceStateChangedReceiver(); 75 76 @NonNull private Map<String, BluetoothDevice> mAddressToBondedDevice = new HashMap<>(); 77 @NonNull private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>(); 78 79 @NonNull 80 private final Context mContext; 81 @NonNull private final Handler mHandler; 82 @NonNull private final BluetoothAdapter mBluetoothAdapter; 83 @NonNull 84 private final BluetoothRouteController.BluetoothRoutesUpdatedListener mListener; 85 @NonNull 86 private final BluetoothProfileMonitor mBluetoothProfileMonitor; 87 BluetoothDeviceRoutesManager( @onNull Context context, @NonNull Handler handler, @NonNull BluetoothAdapter bluetoothAdapter, @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener)88 BluetoothDeviceRoutesManager( 89 @NonNull Context context, 90 @NonNull Handler handler, 91 @NonNull BluetoothAdapter bluetoothAdapter, 92 @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) { 93 this( 94 context, 95 handler, 96 bluetoothAdapter, 97 new BluetoothProfileMonitor(context, bluetoothAdapter), 98 listener); 99 } 100 101 @VisibleForTesting BluetoothDeviceRoutesManager( @onNull Context context, @NonNull Handler handler, @NonNull BluetoothAdapter bluetoothAdapter, @NonNull BluetoothProfileMonitor bluetoothProfileMonitor, @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener)102 BluetoothDeviceRoutesManager( 103 @NonNull Context context, 104 @NonNull Handler handler, 105 @NonNull BluetoothAdapter bluetoothAdapter, 106 @NonNull BluetoothProfileMonitor bluetoothProfileMonitor, 107 @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) { 108 mContext = Objects.requireNonNull(context); 109 mHandler = handler; 110 mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); 111 mBluetoothProfileMonitor = Objects.requireNonNull(bluetoothProfileMonitor); 112 mListener = Objects.requireNonNull(listener); 113 } 114 start(UserHandle user)115 public void start(UserHandle user) { 116 mBluetoothProfileMonitor.start(); 117 118 IntentFilter adapterStateChangedIntentFilter = new IntentFilter(); 119 120 adapterStateChangedIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 121 mContext.registerReceiverAsUser(mAdapterStateChangedReceiver, user, 122 adapterStateChangedIntentFilter, null, null); 123 124 IntentFilter deviceStateChangedIntentFilter = new IntentFilter(); 125 126 deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 127 deviceStateChangedIntentFilter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED); 128 deviceStateChangedIntentFilter.addAction( 129 BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); 130 deviceStateChangedIntentFilter.addAction( 131 BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED); 132 deviceStateChangedIntentFilter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED); 133 134 mContext.registerReceiverAsUser(mDeviceStateChangedReceiver, user, 135 deviceStateChangedIntentFilter, null, null); 136 updateBluetoothRoutes(); 137 } 138 stop()139 public void stop() { 140 mContext.unregisterReceiver(mAdapterStateChangedReceiver); 141 mContext.unregisterReceiver(mDeviceStateChangedReceiver); 142 } 143 144 /** Returns true if the given address corresponds to a currently-bonded Bluetooth device. */ containsBondedDeviceWithAddress(@ullable String address)145 public synchronized boolean containsBondedDeviceWithAddress(@Nullable String address) { 146 return mAddressToBondedDevice.containsKey(address); 147 } 148 149 @Nullable getRouteIdForBluetoothAddress(@ullable String address)150 public synchronized String getRouteIdForBluetoothAddress(@Nullable String address) { 151 BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address); 152 return bluetoothDevice != null 153 ? getRouteIdForType(bluetoothDevice, getDeviceType(bluetoothDevice)) 154 : null; 155 } 156 157 @Nullable getNameForBluetoothAddress(@onNull String address)158 public synchronized String getNameForBluetoothAddress(@NonNull String address) { 159 BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address); 160 return bluetoothDevice != null ? getDeviceName(bluetoothDevice) : null; 161 } 162 activateBluetoothDeviceWithAddress(String address)163 public synchronized void activateBluetoothDeviceWithAddress(String address) { 164 BluetoothRouteInfo btRouteInfo = mBluetoothRoutes.get(address); 165 166 if (btRouteInfo == null) { 167 Slog.w(TAG, "activateBluetoothDeviceWithAddress: Ignoring unknown address " + address); 168 return; 169 } 170 mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO); 171 } 172 updateBluetoothRoutes()173 private void updateBluetoothRoutes() { 174 Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices(); 175 176 synchronized (this) { 177 mBluetoothRoutes.clear(); 178 if (bondedDevices == null) { 179 // Bonded devices is null upon running into a BluetoothAdapter error. 180 Log.w(TAG, "BluetoothAdapter.getBondedDevices returned null."); 181 return; 182 } 183 // We don't clear bonded devices if we receive a null getBondedDevices result, because 184 // that probably means that the bluetooth stack ran into an issue. Not that all devices 185 // have been unpaired. 186 mAddressToBondedDevice = 187 bondedDevices.stream() 188 .collect( 189 Collectors.toMap( 190 BluetoothDevice::getAddress, Function.identity())); 191 for (BluetoothDevice device : bondedDevices) { 192 if (device.isConnected()) { 193 BluetoothRouteInfo newBtRoute = createBluetoothRoute(device); 194 if (newBtRoute.mConnectedProfiles.size() > 0) { 195 mBluetoothRoutes.put(device.getAddress(), newBtRoute); 196 } 197 } 198 } 199 } 200 } 201 202 @NonNull getAvailableBluetoothRoutes()203 public List<MediaRoute2Info> getAvailableBluetoothRoutes() { 204 List<MediaRoute2Info> routes = new ArrayList<>(); 205 Set<String> routeIds = new HashSet<>(); 206 207 synchronized (this) { 208 for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { 209 // See createBluetoothRoute for info on why we do this. 210 if (routeIds.add(btRoute.mRoute.getId())) { 211 routes.add(btRoute.mRoute); 212 } 213 } 214 } 215 return routes; 216 } 217 notifyBluetoothRoutesUpdated()218 private void notifyBluetoothRoutesUpdated() { 219 mListener.onBluetoothRoutesUpdated(); 220 } 221 222 /** 223 * Creates a new {@link BluetoothRouteInfo}, including its member {@link 224 * BluetoothRouteInfo#mRoute}. 225 * 226 * <p>The most important logic in this method is around the {@link MediaRoute2Info#getId() route 227 * id} assignment. In some cases we want to group multiple {@link BluetoothDevice bluetooth 228 * devices} as a single media route. For example, the left and right hearing aids get exposed as 229 * two different BluetoothDevice instances, but we want to show them as a single route. In this 230 * case, we assign the same route id to all "group" bluetooth devices (like left and right 231 * hearing aids), so that a single route is exposed for both of them. 232 * 233 * <p>Deduplication by id happens downstream because we need to be able to refer to all 234 * bluetooth devices individually, since the audio stack refers to a bluetooth device group by 235 * any of its member devices. 236 */ createBluetoothRoute(BluetoothDevice device)237 private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) { 238 BluetoothRouteInfo 239 newBtRoute = new BluetoothRouteInfo(); 240 newBtRoute.mBtDevice = device; 241 String deviceName = getDeviceName(device); 242 243 int type = getDeviceType(device); 244 String routeId = getRouteIdForType(device, type); 245 246 newBtRoute.mConnectedProfiles = getConnectedProfiles(device); 247 // Note that volume is only relevant for active bluetooth routes, and those are managed via 248 // AudioManager. 249 newBtRoute.mRoute = 250 new MediaRoute2Info.Builder(routeId, deviceName) 251 .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) 252 .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK) 253 .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED) 254 .setDescription( 255 mContext.getResources() 256 .getText(R.string.bluetooth_a2dp_audio_route_name) 257 .toString()) 258 .setType(type) 259 .setAddress(device.getAddress()) 260 .build(); 261 return newBtRoute; 262 } 263 getDeviceName(BluetoothDevice device)264 private String getDeviceName(BluetoothDevice device) { 265 String deviceName = 266 Flags.enableUseOfBluetoothDeviceGetAliasForMr2infoGetName() 267 ? device.getAlias() 268 : device.getName(); 269 if (TextUtils.isEmpty(deviceName)) { 270 deviceName = mContext.getResources().getText(R.string.unknownName).toString(); 271 } 272 return deviceName; 273 } getConnectedProfiles(@onNull BluetoothDevice device)274 private SparseBooleanArray getConnectedProfiles(@NonNull BluetoothDevice device) { 275 SparseBooleanArray connectedProfiles = new SparseBooleanArray(); 276 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.A2DP, device)) { 277 connectedProfiles.put(BluetoothProfile.A2DP, true); 278 } 279 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) { 280 connectedProfiles.put(BluetoothProfile.HEARING_AID, true); 281 } 282 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.LE_AUDIO, device)) { 283 connectedProfiles.put(BluetoothProfile.LE_AUDIO, true); 284 } 285 286 return connectedProfiles; 287 } 288 getDeviceType(@onNull BluetoothDevice device)289 private int getDeviceType(@NonNull BluetoothDevice device) { 290 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.LE_AUDIO, device)) { 291 return MediaRoute2Info.TYPE_BLE_HEADSET; 292 } 293 294 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) { 295 return MediaRoute2Info.TYPE_HEARING_AID; 296 } 297 298 return MediaRoute2Info.TYPE_BLUETOOTH_A2DP; 299 } 300 getRouteIdForType(@onNull BluetoothDevice device, int type)301 private String getRouteIdForType(@NonNull BluetoothDevice device, int type) { 302 return switch (type) { 303 case (MediaRoute2Info.TYPE_BLE_HEADSET) -> 304 LE_AUDIO_ROUTE_ID_PREFIX 305 + mBluetoothProfileMonitor.getGroupId( 306 BluetoothProfile.LE_AUDIO, device); 307 case (MediaRoute2Info.TYPE_HEARING_AID) -> 308 HEARING_AID_ROUTE_ID_PREFIX 309 + mBluetoothProfileMonitor.getGroupId( 310 BluetoothProfile.HEARING_AID, device); 311 // TYPE_BLUETOOTH_A2DP 312 default -> device.getAddress(); 313 }; 314 } 315 handleBluetoothAdapterStateChange(int state)316 private void handleBluetoothAdapterStateChange(int state) { 317 if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) { 318 synchronized (BluetoothDeviceRoutesManager.this) { 319 mBluetoothRoutes.clear(); 320 } 321 notifyBluetoothRoutesUpdated(); 322 } else if (state == BluetoothAdapter.STATE_ON) { 323 updateBluetoothRoutes(); 324 325 boolean shouldCallListener; 326 synchronized (BluetoothDeviceRoutesManager.this) { 327 shouldCallListener = !mBluetoothRoutes.isEmpty(); 328 } 329 330 if (shouldCallListener) { 331 notifyBluetoothRoutesUpdated(); 332 } 333 } 334 } 335 336 private static class BluetoothRouteInfo { 337 private BluetoothDevice mBtDevice; 338 private MediaRoute2Info mRoute; 339 private SparseBooleanArray mConnectedProfiles; 340 } 341 342 private class AdapterStateChangedReceiver extends BroadcastReceiver { 343 @Override onReceive(Context context, Intent intent)344 public void onReceive(Context context, Intent intent) { 345 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 346 if (Flags.enableMr2ServiceNonMainBgThread()) { 347 mHandler.post(() -> handleBluetoothAdapterStateChange(state)); 348 } else { 349 handleBluetoothAdapterStateChange(state); 350 } 351 } 352 } 353 354 private class DeviceStateChangedReceiver extends BroadcastReceiver { 355 @Override onReceive(Context context, Intent intent)356 public void onReceive(Context context, Intent intent) { 357 switch (intent.getAction()) { 358 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 359 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: 360 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED: 361 case BluetoothDevice.ACTION_ALIAS_CHANGED: 362 if (Flags.enableMr2ServiceNonMainBgThread()) { 363 mHandler.post( 364 () -> { 365 updateBluetoothRoutes(); 366 notifyBluetoothRoutesUpdated(); 367 }); 368 } else { 369 updateBluetoothRoutes(); 370 notifyBluetoothRoutesUpdated(); 371 } 372 } 373 } 374 } 375 } 376