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 @Nullable getRouteIdForBluetoothAddress(@ullable String address)145 public synchronized String getRouteIdForBluetoothAddress(@Nullable String address) { 146 BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address); 147 return bluetoothDevice != null 148 ? getRouteIdForType(bluetoothDevice, getDeviceType(bluetoothDevice)) 149 : null; 150 } 151 152 @Nullable getNameForBluetoothAddress(@onNull String address)153 public synchronized String getNameForBluetoothAddress(@NonNull String address) { 154 BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address); 155 return bluetoothDevice != null ? getDeviceName(bluetoothDevice) : null; 156 } 157 activateBluetoothDeviceWithAddress(String address)158 public synchronized void activateBluetoothDeviceWithAddress(String address) { 159 BluetoothRouteInfo btRouteInfo = mBluetoothRoutes.get(address); 160 161 if (btRouteInfo == null) { 162 Slog.w(TAG, "activateBluetoothDeviceWithAddress: Ignoring unknown address " + address); 163 return; 164 } 165 mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO); 166 } 167 updateBluetoothRoutes()168 private void updateBluetoothRoutes() { 169 Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices(); 170 171 synchronized (this) { 172 mBluetoothRoutes.clear(); 173 if (bondedDevices == null) { 174 // Bonded devices is null upon running into a BluetoothAdapter error. 175 Log.w(TAG, "BluetoothAdapter.getBondedDevices returned null."); 176 return; 177 } 178 // We don't clear bonded devices if we receive a null getBondedDevices result, because 179 // that probably means that the bluetooth stack ran into an issue. Not that all devices 180 // have been unpaired. 181 mAddressToBondedDevice = 182 bondedDevices.stream() 183 .collect( 184 Collectors.toMap( 185 BluetoothDevice::getAddress, Function.identity())); 186 for (BluetoothDevice device : bondedDevices) { 187 if (device.isConnected()) { 188 BluetoothRouteInfo newBtRoute = createBluetoothRoute(device); 189 if (newBtRoute.mConnectedProfiles.size() > 0) { 190 mBluetoothRoutes.put(device.getAddress(), newBtRoute); 191 } 192 } 193 } 194 } 195 } 196 197 @NonNull getAvailableBluetoothRoutes()198 public List<MediaRoute2Info> getAvailableBluetoothRoutes() { 199 List<MediaRoute2Info> routes = new ArrayList<>(); 200 Set<String> routeIds = new HashSet<>(); 201 202 synchronized (this) { 203 for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { 204 // See createBluetoothRoute for info on why we do this. 205 if (routeIds.add(btRoute.mRoute.getId())) { 206 routes.add(btRoute.mRoute); 207 } 208 } 209 } 210 return routes; 211 } 212 notifyBluetoothRoutesUpdated()213 private void notifyBluetoothRoutesUpdated() { 214 mListener.onBluetoothRoutesUpdated(); 215 } 216 217 /** 218 * Creates a new {@link BluetoothRouteInfo}, including its member {@link 219 * BluetoothRouteInfo#mRoute}. 220 * 221 * <p>The most important logic in this method is around the {@link MediaRoute2Info#getId() route 222 * id} assignment. In some cases we want to group multiple {@link BluetoothDevice bluetooth 223 * devices} as a single media route. For example, the left and right hearing aids get exposed as 224 * two different BluetoothDevice instances, but we want to show them as a single route. In this 225 * case, we assign the same route id to all "group" bluetooth devices (like left and right 226 * hearing aids), so that a single route is exposed for both of them. 227 * 228 * <p>Deduplication by id happens downstream because we need to be able to refer to all 229 * bluetooth devices individually, since the audio stack refers to a bluetooth device group by 230 * any of its member devices. 231 */ createBluetoothRoute(BluetoothDevice device)232 private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) { 233 BluetoothRouteInfo 234 newBtRoute = new BluetoothRouteInfo(); 235 newBtRoute.mBtDevice = device; 236 String deviceName = getDeviceName(device); 237 238 int type = getDeviceType(device); 239 String routeId = getRouteIdForType(device, type); 240 241 newBtRoute.mConnectedProfiles = getConnectedProfiles(device); 242 // Note that volume is only relevant for active bluetooth routes, and those are managed via 243 // AudioManager. 244 newBtRoute.mRoute = 245 new MediaRoute2Info.Builder(routeId, deviceName) 246 .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) 247 .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK) 248 .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED) 249 .setDescription( 250 mContext.getResources() 251 .getText(R.string.bluetooth_a2dp_audio_route_name) 252 .toString()) 253 .setType(type) 254 .setAddress(device.getAddress()) 255 .build(); 256 return newBtRoute; 257 } 258 getDeviceName(BluetoothDevice device)259 private String getDeviceName(BluetoothDevice device) { 260 String deviceName = 261 Flags.enableUseOfBluetoothDeviceGetAliasForMr2infoGetName() 262 ? device.getAlias() 263 : device.getName(); 264 if (TextUtils.isEmpty(deviceName)) { 265 deviceName = mContext.getResources().getText(R.string.unknownName).toString(); 266 } 267 return deviceName; 268 } getConnectedProfiles(@onNull BluetoothDevice device)269 private SparseBooleanArray getConnectedProfiles(@NonNull BluetoothDevice device) { 270 SparseBooleanArray connectedProfiles = new SparseBooleanArray(); 271 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.A2DP, device)) { 272 connectedProfiles.put(BluetoothProfile.A2DP, true); 273 } 274 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) { 275 connectedProfiles.put(BluetoothProfile.HEARING_AID, true); 276 } 277 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.LE_AUDIO, device)) { 278 connectedProfiles.put(BluetoothProfile.LE_AUDIO, true); 279 } 280 281 return connectedProfiles; 282 } 283 getDeviceType(@onNull BluetoothDevice device)284 private int getDeviceType(@NonNull BluetoothDevice device) { 285 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.LE_AUDIO, device)) { 286 return MediaRoute2Info.TYPE_BLE_HEADSET; 287 } 288 289 if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) { 290 return MediaRoute2Info.TYPE_HEARING_AID; 291 } 292 293 return MediaRoute2Info.TYPE_BLUETOOTH_A2DP; 294 } 295 getRouteIdForType(@onNull BluetoothDevice device, int type)296 private String getRouteIdForType(@NonNull BluetoothDevice device, int type) { 297 return switch (type) { 298 case (MediaRoute2Info.TYPE_BLE_HEADSET) -> 299 LE_AUDIO_ROUTE_ID_PREFIX 300 + mBluetoothProfileMonitor.getGroupId( 301 BluetoothProfile.LE_AUDIO, device); 302 case (MediaRoute2Info.TYPE_HEARING_AID) -> 303 HEARING_AID_ROUTE_ID_PREFIX 304 + mBluetoothProfileMonitor.getGroupId( 305 BluetoothProfile.HEARING_AID, device); 306 // TYPE_BLUETOOTH_A2DP 307 default -> device.getAddress(); 308 }; 309 } 310 handleBluetoothAdapterStateChange(int state)311 private void handleBluetoothAdapterStateChange(int state) { 312 if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) { 313 synchronized (BluetoothDeviceRoutesManager.this) { 314 mBluetoothRoutes.clear(); 315 } 316 notifyBluetoothRoutesUpdated(); 317 } else if (state == BluetoothAdapter.STATE_ON) { 318 updateBluetoothRoutes(); 319 320 boolean shouldCallListener; 321 synchronized (BluetoothDeviceRoutesManager.this) { 322 shouldCallListener = !mBluetoothRoutes.isEmpty(); 323 } 324 325 if (shouldCallListener) { 326 notifyBluetoothRoutesUpdated(); 327 } 328 } 329 } 330 331 private static class BluetoothRouteInfo { 332 private BluetoothDevice mBtDevice; 333 private MediaRoute2Info mRoute; 334 private SparseBooleanArray mConnectedProfiles; 335 } 336 337 private class AdapterStateChangedReceiver extends BroadcastReceiver { 338 @Override onReceive(Context context, Intent intent)339 public void onReceive(Context context, Intent intent) { 340 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 341 if (Flags.enableMr2ServiceNonMainBgThread()) { 342 mHandler.post(() -> handleBluetoothAdapterStateChange(state)); 343 } else { 344 handleBluetoothAdapterStateChange(state); 345 } 346 } 347 } 348 349 private class DeviceStateChangedReceiver extends BroadcastReceiver { 350 @Override onReceive(Context context, Intent intent)351 public void onReceive(Context context, Intent intent) { 352 switch (intent.getAction()) { 353 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 354 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: 355 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED: 356 case BluetoothDevice.ACTION_ALIAS_CHANGED: 357 if (Flags.enableMr2ServiceNonMainBgThread()) { 358 mHandler.post( 359 () -> { 360 updateBluetoothRoutes(); 361 notifyBluetoothRoutesUpdated(); 362 }); 363 } else { 364 updateBluetoothRoutes(); 365 notifyBluetoothRoutesUpdated(); 366 } 367 } 368 } 369 } 370 } 371