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