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