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.BluetoothClass; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothProfile; 23 import android.content.Context; 24 import android.util.Log; 25 26 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 27 import com.android.settingslib.bluetooth.LocalBluetoothManager; 28 29 import java.util.Arrays; 30 import java.util.Collections; 31 import java.util.List; 32 import java.util.concurrent.ExecutionException; 33 import java.util.concurrent.FutureTask; 34 35 public class BluetoothUtils { 36 37 private static final String TAG = "Atv.BluetoothUtils"; 38 39 private static List<String> sKnownRemoteLabels = null; 40 private static final int MINOR_MASK = 0b11111100; 41 42 private static final int MINOR_DEVICE_CLASS_POINTING = 0b10000000; 43 private static final int MINOR_DEVICE_CLASS_KEYBOARD = 0b01000000; 44 private static final int MINOR_DEVICE_CLASS_JOYSTICK = 0b00000100; 45 private static final int MINOR_DEVICE_CLASS_GAMEPAD = 0b00001000; 46 private static final int MINOR_DEVICE_CLASS_REMOTE = 0b00001100; 47 48 // Includes any generic keyboards or pointers, and any joystick, game pad, or remote subtypes. 49 private static final int MINOR_REMOTE_MASK = 0b11001100; 50 isRemoteClass(BluetoothDevice device)51 public static boolean isRemoteClass(BluetoothDevice device) { 52 if (device == null) { 53 return false; 54 } 55 int major = device.getBluetoothClass().getMajorDeviceClass(); 56 int minor = device.getBluetoothClass().getDeviceClass() & MINOR_MASK; 57 return BluetoothClass.Device.Major.PERIPHERAL == major 58 && (minor & ~MINOR_REMOTE_MASK) == 0; 59 } 60 setKnownRemoteLabels(Context context)61 private static void setKnownRemoteLabels(Context context) { 62 if (context == null) { 63 return; 64 } 65 sKnownRemoteLabels = Collections.unmodifiableList(Arrays.asList( 66 context.getResources().getStringArray(R.array.known_bluetooth_device_labels))); 67 // For backward compatibility, the customization name used to be known_remote_labels 68 if (sKnownRemoteLabels.isEmpty()) { 69 sKnownRemoteLabels = Collections.unmodifiableList( 70 Arrays.asList( 71 context.getResources().getStringArray( 72 R.array.known_remote_labels))); 73 } 74 } 75 isConnected(BluetoothDevice device)76 public static boolean isConnected(BluetoothDevice device) { 77 if (device == null) { 78 return false; 79 } 80 return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected(); 81 } 82 isBonded(BluetoothDevice device)83 public static boolean isBonded(BluetoothDevice device) { 84 if (device == null) { 85 return false; 86 } 87 return device.getBondState() == BluetoothDevice.BOND_BONDED && !device.isConnected(); 88 } 89 getName(BluetoothDevice device)90 public static String getName(BluetoothDevice device) { 91 if (device == null) { 92 return null; 93 } 94 return device.getAlias() != null ? device.getAlias() : device.getName(); 95 } 96 getOriginalName(BluetoothDevice device)97 public static String getOriginalName(BluetoothDevice device) { 98 if (device == null) { 99 return null; 100 } 101 return device.getName(); 102 } 103 isRemote(Context context, BluetoothDevice device)104 public static boolean isRemote(Context context, BluetoothDevice device) { 105 if (sKnownRemoteLabels == null) { 106 setKnownRemoteLabels(context); 107 } 108 if (device == null) { 109 return false; 110 } 111 if (device.getName() == null) { 112 return false; 113 } 114 115 if (sKnownRemoteLabels == null) { 116 return false; 117 } 118 119 final String name = device.getName().toLowerCase(); 120 for (String knownLabel: sKnownRemoteLabels) { 121 if (name.contains(knownLabel)) { 122 return true; 123 } 124 } 125 return false; 126 } 127 isBluetoothHeadset(BluetoothDevice device)128 public static boolean isBluetoothHeadset(BluetoothDevice device) { 129 if (device == null) { 130 return false; 131 } 132 final BluetoothClass bluetoothClass = device.getBluetoothClass(); 133 final int devClass = bluetoothClass.getDeviceClass(); 134 return (devClass == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET || 135 devClass == BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES || 136 devClass == BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER || 137 devClass == BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO || 138 devClass == BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO); 139 } 140 isLeCompatible(BluetoothDevice device)141 public static boolean isLeCompatible(BluetoothDevice device) { 142 return device != null && (device.getType() == BluetoothDevice.DEVICE_TYPE_LE || 143 device.getType() == BluetoothDevice.DEVICE_TYPE_DUAL); 144 } 145 146 @SuppressLint("NewApi") // Hidden API made public isA2dpSource(BluetoothDevice device)147 public static boolean isA2dpSource(BluetoothDevice device) { 148 return device != null && device.getBluetoothClass() != null && 149 device.getBluetoothClass().doesClassMatch(BluetoothProfile.A2DP); 150 } 151 152 /** 153 * Match a device's metadata against a predefined list to determine whether the device is an 154 * official device to be used with the host device. 155 */ isOfficialDevice(Context context, BluetoothDevice device)156 public static boolean isOfficialDevice(Context context, BluetoothDevice device) { 157 boolean isManufacturerOfficial = 158 isBluetoothDeviceMetadataInList( 159 context, 160 device, 161 BluetoothDevice.METADATA_MANUFACTURER_NAME, 162 R.array.official_bt_device_manufacturer_names); 163 boolean isModelOfficial = 164 isBluetoothDeviceMetadataInList( 165 context, 166 device, 167 BluetoothDevice.METADATA_MODEL_NAME, 168 R.array.official_bt_device_model_names); 169 return isManufacturerOfficial && isModelOfficial; 170 } 171 isOfficialRemote(Context context, BluetoothDevice device)172 public static boolean isOfficialRemote(Context context, BluetoothDevice device) { 173 return isRemote(context, device) && isOfficialDevice(context, device); 174 } 175 getIcon(Context context, BluetoothDevice device)176 public static int getIcon(Context context, BluetoothDevice device) { 177 if (device == null) { 178 return 0; 179 } 180 final BluetoothClass bluetoothClass = device.getBluetoothClass(); 181 final int devClass = bluetoothClass.getDeviceClass(); 182 // Below ordering does matter 183 if (isOfficialRemote(context, device)) { 184 return R.drawable.ic_official_remote; 185 } else if (isRemote(context, device)) { 186 return R.drawable.ic_games; 187 } else if (isBluetoothHeadset(device)) { 188 return R.drawable.ic_headset; 189 } else if ((devClass & MINOR_DEVICE_CLASS_POINTING) != 0) { 190 return R.drawable.ic_mouse; 191 } else if (isA2dpSource(device)) { 192 return R.drawable.ic_baseline_smartphone_24dp; 193 } else if ((devClass & MINOR_DEVICE_CLASS_REMOTE) != 0) { 194 return R.drawable.ic_games; 195 } else if ((devClass & MINOR_DEVICE_CLASS_JOYSTICK) != 0) { 196 return R.drawable.ic_games; 197 } else if ((devClass & MINOR_DEVICE_CLASS_GAMEPAD) != 0) { 198 return R.drawable.ic_games; 199 } else if ((devClass & MINOR_DEVICE_CLASS_KEYBOARD) != 0) { 200 return R.drawable.ic_keyboard; 201 } 202 // Default for now 203 return R.drawable.ic_bluetooth; 204 } 205 206 /** 207 * @param context the context 208 * @param device the bluetooth device 209 * @param metadataKey one of BluetoothDevice.METADATA_* 210 * @param stringArrayResId resource Id of <string-array> to match the metadata against 211 * @return whether the specified metadata in within the list of stringArrayResId. 212 */ isBluetoothDeviceMetadataInList( Context context, BluetoothDevice device, int metadataKey, int stringArrayResId)213 public static boolean isBluetoothDeviceMetadataInList( 214 Context context, BluetoothDevice device, int metadataKey, int stringArrayResId) { 215 if (context == null || device == null) { 216 return false; 217 } 218 byte[] metadataBytes = device.getMetadata(metadataKey); 219 if (metadataBytes == null) { 220 return false; 221 } 222 final List<String> stringResList = 223 Arrays.asList(context.getResources().getStringArray(stringArrayResId)); 224 if (stringResList == null || stringResList.isEmpty()) { 225 return false; 226 } 227 for (String res : stringResList) { 228 if (res.equals(new String(metadataBytes))) { 229 return true; 230 } 231 } 232 return false; 233 } 234 getBluetoothDeviceServiceClass(Context context)235 public static Class getBluetoothDeviceServiceClass(Context context) { 236 String str = context.getString(R.string.bluetooth_device_service_class); 237 try { 238 return Class.forName(str); 239 } catch (ClassNotFoundException e) { 240 Log.e(TAG, "Class not found: " + str); 241 return null; 242 } 243 } 244 getLocalBluetoothManager(Context context)245 public static LocalBluetoothManager getLocalBluetoothManager(Context context) { 246 final FutureTask<LocalBluetoothManager> localBluetoothManagerFutureTask = 247 new FutureTask<>( 248 // Avoid StrictMode ThreadPolicy violation 249 () -> LocalBluetoothManager.getInstance( 250 context, (c, bluetoothManager) -> {})); 251 try { 252 localBluetoothManagerFutureTask.run(); 253 return localBluetoothManagerFutureTask.get(); 254 } catch (InterruptedException | ExecutionException e) { 255 Log.w(TAG, "Error getting LocalBluetoothManager.", e); 256 return null; 257 } 258 } 259 getCachedBluetoothDevice( Context context, BluetoothDevice device)260 public static CachedBluetoothDevice getCachedBluetoothDevice( 261 Context context, BluetoothDevice device) { 262 LocalBluetoothManager localBluetoothManager = getLocalBluetoothManager(context); 263 if (localBluetoothManager != null) { 264 return localBluetoothManager.getCachedDeviceManager().findDevice(device); 265 } 266 return null; 267 } 268 } 269