• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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