1 /*
2  * Copyright (C) 2021 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.google.android.tv.btservices;
18 
19 import android.annotation.SuppressLint;
20 import android.bluetooth.BluetoothAdapter;
21 import android.bluetooth.BluetoothClass;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothProfile;
24 import android.content.Context;
25 import android.util.Log;
26 
27 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
28 import com.android.settingslib.bluetooth.LocalBluetoothManager;
29 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
30 
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.concurrent.ExecutionException;
35 import java.util.concurrent.FutureTask;
36 
37 public class BluetoothUtils {
38 
39     private static final String TAG = "Atv.BluetoothUtils";
40 
41     private static List<String> sKnownRemoteLabels = null;
42     private static List<String> sOfficialRemoteLabels = null;
43     private static List<String> sBtDeviceServiceUpdatableRemoteLabels = null;
44     private static final int MINOR_MASK = 0b11111100;
45 
46     private static final int MINOR_DEVICE_CLASS_POINTING = 0b10000000;
47     private static final int MINOR_DEVICE_CLASS_KEYBOARD = 0b01000000;
48     private static final int MINOR_DEVICE_CLASS_JOYSTICK = 0b00000100;
49     private static final int MINOR_DEVICE_CLASS_GAMEPAD = 0b00001000;
50     private static final int MINOR_DEVICE_CLASS_REMOTE = 0b00001100;
51 
52     // Includes any generic keyboards or pointers, and any joystick, game pad, or remote subtypes.
53     private static final int MINOR_REMOTE_MASK = 0b11001100;
54 
isRemoteClass(BluetoothDevice device)55     public static boolean isRemoteClass(BluetoothDevice device) {
56         if (device == null) {
57             return false;
58         }
59         int major = device.getBluetoothClass().getMajorDeviceClass();
60         int minor = device.getBluetoothClass().getDeviceClass() & MINOR_MASK;
61         return BluetoothClass.Device.Major.PERIPHERAL == major
62             && (minor & ~MINOR_REMOTE_MASK) == 0;
63     }
64 
setRemoteLabels(Context context)65     private static void setRemoteLabels(Context context) {
66         if (context == null) {
67             return;
68         }
69         sKnownRemoteLabels = Collections.unmodifiableList(Arrays.asList(
70                 context.getResources().getStringArray(R.array.known_bluetooth_device_labels)));
71         // For backward compatibility, the customization name used to be known_remote_labels
72         if (sKnownRemoteLabels.isEmpty()) {
73             sKnownRemoteLabels = Collections.unmodifiableList(
74                     Arrays.asList(
75                         context.getResources().getStringArray(
76                             R.array.known_remote_labels)));
77         }
78 
79         sOfficialRemoteLabels = Collections.unmodifiableList(Arrays.asList(
80                 context.getResources().getStringArray(R.array.official_bt_device_labels)));
81 
82         sBtDeviceServiceUpdatableRemoteLabels = Collections.unmodifiableList(Arrays.asList(
83                 context.getResources().getStringArray(R.array.bt_device_service_updatable_labels)));
84     }
85 
isConnected(BluetoothDevice device)86     public static boolean isConnected(BluetoothDevice device) {
87         if (device == null) {
88             return false;
89         }
90         return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected();
91     }
92 
isBonded(BluetoothDevice device)93     public static boolean isBonded(BluetoothDevice device) {
94         if (device == null) {
95             return false;
96         }
97         return device.getBondState() == BluetoothDevice.BOND_BONDED && !device.isConnected();
98     }
99 
getName(BluetoothDevice device)100     public static String getName(BluetoothDevice device) {
101         if (device == null) {
102             return null;
103         }
104         return device.getAlias() != null ? device.getAlias() : device.getName();
105     }
106 
getOriginalName(BluetoothDevice device)107     public static String getOriginalName(BluetoothDevice device) {
108         if (device == null) {
109             return null;
110         }
111         return device.getName();
112     }
113 
isRemote(Context context, BluetoothDevice device)114     public static boolean isRemote(Context context, BluetoothDevice device) {
115         if (sKnownRemoteLabels == null) {
116             setRemoteLabels(context);
117         }
118         if (device == null) {
119             return false;
120         }
121         if (device.getName() == null) {
122             return false;
123         }
124 
125         if (sKnownRemoteLabels == null) {
126             return false;
127         }
128 
129         final String name = device.getName().toLowerCase();
130         for (String knownLabel: sKnownRemoteLabels) {
131             if (name.contains(knownLabel)) {
132                 return true;
133             }
134         }
135         return false;
136     }
137 
isBluetoothHeadset(BluetoothDevice device)138     public static boolean isBluetoothHeadset(BluetoothDevice device) {
139         if (device == null) {
140             return false;
141         }
142         final BluetoothClass bluetoothClass = device.getBluetoothClass();
143         final int devClass = bluetoothClass.getDeviceClass();
144         return (devClass == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET ||
145                 devClass == BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES ||
146                 devClass == BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER ||
147                 devClass == BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO ||
148                 devClass == BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO);
149     }
150 
isLeCompatible(BluetoothDevice device)151     public static boolean isLeCompatible(BluetoothDevice device) {
152         return device != null && (device.getType() == BluetoothDevice.DEVICE_TYPE_LE ||
153                 device.getType() == BluetoothDevice.DEVICE_TYPE_DUAL);
154     }
155 
156     @SuppressLint("NewApi") // Hidden API made public
isA2dpSource(BluetoothDevice device)157     public static boolean isA2dpSource(BluetoothDevice device) {
158         return device != null && device.getBluetoothClass() != null &&
159                 device.getBluetoothClass().doesClassMatch(BluetoothProfile.A2DP);
160     }
161 
162     /**
163      * If a device's metadata (manufacturer, model) or name matches against a predefined list,
164      * treat it as an official device to be used with the host device.
165      */
isOfficialDevice(Context context, BluetoothDevice device)166     public static boolean isOfficialDevice(Context context, BluetoothDevice device) {
167         boolean isManufacturerOfficial =
168                 isBluetoothDeviceMetadataInList(
169                         context,
170                         device,
171                         BluetoothDevice.METADATA_MANUFACTURER_NAME,
172                         R.array.official_bt_device_manufacturer_names);
173         boolean isModelOfficial =
174                 isBluetoothDeviceMetadataInList(
175                         context,
176                         device,
177                         BluetoothDevice.METADATA_MODEL_NAME,
178                         R.array.official_bt_device_model_names);
179 
180         if (isManufacturerOfficial && isModelOfficial) {
181             return true;
182         }
183 
184         if (sOfficialRemoteLabels == null) {
185             setRemoteLabels(context);
186         }
187 
188         if (device == null || device.getName() == null) {
189             return false;
190         }
191 
192         if (sOfficialRemoteLabels == null) {
193             return false;
194         }
195 
196         final String name = device.getName().toLowerCase();
197         for (String knownLabel : sOfficialRemoteLabels) {
198             if (name.contains(knownLabel)) {
199                 return true;
200             }
201         }
202         return false;
203     }
204 
isOfficialRemote(Context context, BluetoothDevice device)205     public static boolean isOfficialRemote(Context context, BluetoothDevice device) {
206         return isRemote(context, device) && isOfficialDevice(context, device);
207     }
208 
isBtDeviceServiceUpdableRemote(Context context, BluetoothDevice device)209     public static boolean isBtDeviceServiceUpdableRemote(Context context, BluetoothDevice device) {
210         if (sBtDeviceServiceUpdatableRemoteLabels == null) {
211             setRemoteLabels(context);
212         }
213 
214         if (device == null || device.getName() == null) {
215             return false;
216         }
217 
218         if (sBtDeviceServiceUpdatableRemoteLabels == null) {
219             return false;
220         }
221 
222         final String name = device.getName().toLowerCase();
223         for (String knownLabel : sBtDeviceServiceUpdatableRemoteLabels) {
224             if (name.contains(knownLabel)) {
225                 return true;
226             }
227         }
228         return false;
229     }
230 
231 
supportBtDeviceServiceUpdate(Context context, BluetoothDevice device)232     public static boolean supportBtDeviceServiceUpdate(Context context, BluetoothDevice device) {
233         return isRemote(context, device) && isBtDeviceServiceUpdableRemote(context, device);
234     }
235 
getIcon(Context context, BluetoothDevice device)236     public static int getIcon(Context context, BluetoothDevice device) {
237         if (device == null) {
238             return 0;
239         }
240         final BluetoothClass bluetoothClass = device.getBluetoothClass();
241         final int devClass = bluetoothClass.getDeviceClass();
242         // Below ordering does matter
243         if (isOfficialRemote(context, device)) {
244             return R.drawable.ic_official_remote;
245         } else if (isRemote(context, device)) {
246             return R.drawable.ic_games;
247         } else if (isBluetoothHeadset(device)) {
248             return R.drawable.ic_headset;
249         } else if ((devClass & MINOR_DEVICE_CLASS_POINTING) != 0) {
250             return R.drawable.ic_mouse;
251         } else if (isA2dpSource(device)) {
252             return R.drawable.ic_baseline_smartphone_24dp;
253         } else if ((devClass & MINOR_DEVICE_CLASS_REMOTE) != 0) {
254             return R.drawable.ic_games;
255         } else if ((devClass & MINOR_DEVICE_CLASS_JOYSTICK) != 0) {
256             return R.drawable.ic_games;
257         } else if ((devClass & MINOR_DEVICE_CLASS_GAMEPAD) != 0) {
258             return R.drawable.ic_games;
259         } else if ((devClass & MINOR_DEVICE_CLASS_KEYBOARD) != 0) {
260             return R.drawable.ic_keyboard;
261         }
262         // Default for now
263         return R.drawable.ic_bluetooth;
264     }
265 
266     /**
267      * @param context the context
268      * @param device the bluetooth device
269      * @param metadataKey one of BluetoothDevice.METADATA_*
270      * @param stringArrayResId resource Id of <string-array> to match the metadata against
271      * @return whether the specified metadata in within the list of stringArrayResId.
272      */
isBluetoothDeviceMetadataInList( Context context, BluetoothDevice device, int metadataKey, int stringArrayResId)273     public static boolean isBluetoothDeviceMetadataInList(
274             Context context, BluetoothDevice device, int metadataKey, int stringArrayResId) {
275         if (context == null || device == null) {
276             return false;
277         }
278         byte[] metadataBytes = device.getMetadata(metadataKey);
279         if (metadataBytes == null) {
280             return false;
281         }
282         final List<String> stringResList =
283                 Arrays.asList(context.getResources().getStringArray(stringArrayResId));
284         if (stringResList == null || stringResList.isEmpty()) {
285             return false;
286         }
287         for (String res : stringResList) {
288             if (res.equals(new String(metadataBytes))) {
289                 return true;
290             }
291         }
292         return false;
293     }
294 
getBluetoothDeviceServiceClass(Context context)295     public static Class getBluetoothDeviceServiceClass(Context context) {
296         String str = context.getString(R.string.bluetooth_device_service_class);
297         try {
298             return Class.forName(str);
299         } catch (ClassNotFoundException e) {
300             Log.e(TAG, "Class not found: " + str);
301             return null;
302         }
303     }
304 
getLocalBluetoothManager(Context context)305     public static LocalBluetoothManager getLocalBluetoothManager(Context context) {
306         final FutureTask<LocalBluetoothManager> localBluetoothManagerFutureTask =
307                 new FutureTask<>(
308                         // Avoid StrictMode ThreadPolicy violation
309                         () -> LocalBluetoothManager.getInstance(
310                                 context, (c, bluetoothManager) -> {}));
311         try {
312             localBluetoothManagerFutureTask.run();
313             return localBluetoothManagerFutureTask.get();
314         } catch (InterruptedException | ExecutionException e) {
315             Log.w(TAG, "Error getting LocalBluetoothManager.", e);
316             return null;
317         }
318     }
319 
getDefaultBluetoothAdapter()320     public static BluetoothAdapter getDefaultBluetoothAdapter() {
321         final FutureTask<BluetoothAdapter> defaultBluetoothAdapterFutureTask =
322                 new FutureTask<>(
323                         // Avoid StrictMode ThreadPolicy violation
324                         BluetoothAdapter::getDefaultAdapter);
325         try {
326             defaultBluetoothAdapterFutureTask.run();
327             return defaultBluetoothAdapterFutureTask.get();
328         } catch (InterruptedException | ExecutionException e) {
329             Log.w(TAG, "Error getting default BluetoothAdapter.", e);
330             return null;
331         }
332     }
333 
getCachedBluetoothDevice( Context context, BluetoothDevice device)334     public static CachedBluetoothDevice getCachedBluetoothDevice(
335             Context context, BluetoothDevice device) {
336         LocalBluetoothManager localBluetoothManager = getLocalBluetoothManager(context);
337         if (localBluetoothManager != null) {
338             return localBluetoothManager.getCachedDeviceManager().findDevice(device);
339         }
340         return null;
341     }
342 
343     /** Returns true if the BluetoothDevice is the active audio output, false otherwise. */
isActiveAudioOutput(BluetoothDevice device)344     public static boolean isActiveAudioOutput(BluetoothDevice device) {
345         if (device != null) {
346             final BluetoothAdapter btAdapter = getDefaultBluetoothAdapter();
347             if (btAdapter != null) {
348                 return btAdapter.getActiveDevices(BluetoothProfile.A2DP).contains(device);
349             }
350         }
351         return false;
352     }
353 
354     /**
355      * Sets the specified BluetoothDevice as the active audio output. Passing `null`
356      * resets the active audio output to the default. Returns false on immediate error,
357      * true otherwise.
358      */
setActiveAudioOutput(BluetoothDevice device)359     public static boolean setActiveAudioOutput(BluetoothDevice device) {
360         // null is an accepted value for unsetting the active audio output
361         final BluetoothAdapter btAdapter = getDefaultBluetoothAdapter();
362         if (btAdapter != null) {
363             if (device == null) {
364                 return btAdapter.removeActiveDevice(BluetoothAdapter.ACTIVE_DEVICE_AUDIO);
365             } else {
366                 return btAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_AUDIO);
367             }
368         }
369         return false;
370     }
371 
372     /**
373      * Returns true if the CachedBluetoothDevice supports an audio profile (A2DP for now),
374      * false otherwise.
375      */
hasAudioProfile(CachedBluetoothDevice cachedDevice)376     public static boolean hasAudioProfile(CachedBluetoothDevice cachedDevice) {
377       if (cachedDevice != null) {
378           for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
379               if (profile.getProfileId() == BluetoothProfile.A2DP) {
380                   return true;
381               }
382           }
383       }
384       return false;
385     }
386 }
387