1 /* 2 * Copyright (C) 2011 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.settings.bluetooth; 18 19 import static android.os.Process.BLUETOOTH_UID; 20 21 import android.app.settings.SettingsEnums; 22 import android.bluetooth.BluetoothCsipSetCoordinator; 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothProfile; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.content.pm.ActivityInfo; 28 import android.content.pm.PackageInfo; 29 import android.content.pm.PackageManager; 30 import android.content.pm.PackageManager.NameNotFoundException; 31 import android.os.SystemProperties; 32 import android.os.UserHandle; 33 import android.provider.Settings; 34 import android.util.Log; 35 import android.widget.Toast; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.annotation.VisibleForTesting; 40 import androidx.appcompat.app.AlertDialog; 41 42 import com.android.settings.R; 43 import com.android.settings.flags.Flags; 44 import com.android.settings.overlay.FeatureFactory; 45 import com.android.settingslib.bluetooth.BluetoothUtils; 46 import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener; 47 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 48 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 49 import com.android.settingslib.bluetooth.HearingAidProfile; 50 import com.android.settingslib.bluetooth.LeAudioProfile; 51 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; 52 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 53 import com.android.settingslib.bluetooth.LocalBluetoothManager; 54 import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback; 55 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 56 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 57 import com.android.settingslib.utils.ThreadUtils; 58 59 import com.google.common.base.Supplier; 60 import com.google.common.collect.ImmutableList; 61 62 import java.util.ArrayList; 63 import java.util.Collection; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Set; 67 import java.util.concurrent.ExecutionException; 68 import java.util.concurrent.FutureTask; 69 import java.util.stream.Collectors; 70 71 /** 72 * Utils is a helper class that contains constants for various 73 * Android resource IDs, debug logging flags, and static methods 74 * for creating dialogs. 75 */ 76 public final class Utils { 77 78 private static final String TAG = "BluetoothUtils"; 79 private static final String ENABLE_DUAL_MODE_AUDIO = "persist.bluetooth.enable_dual_mode_audio"; 80 81 static final boolean V = BluetoothUtils.V; // verbose logging 82 static final boolean D = BluetoothUtils.D; // regular logging 83 Utils()84 private Utils() { 85 } 86 getConnectionStateSummary(int connectionState)87 public static int getConnectionStateSummary(int connectionState) { 88 switch (connectionState) { 89 case BluetoothProfile.STATE_CONNECTED: 90 return com.android.settingslib.R.string.bluetooth_connected; 91 case BluetoothProfile.STATE_CONNECTING: 92 return com.android.settingslib.R.string.bluetooth_connecting; 93 case BluetoothProfile.STATE_DISCONNECTED: 94 return com.android.settingslib.R.string.bluetooth_disconnected; 95 case BluetoothProfile.STATE_DISCONNECTING: 96 return com.android.settingslib.R.string.bluetooth_disconnecting; 97 default: 98 return 0; 99 } 100 } 101 102 // Create (or recycle existing) and show disconnect dialog. showDisconnectDialog(Context context, AlertDialog dialog, DialogInterface.OnClickListener disconnectListener, CharSequence title, CharSequence message)103 static AlertDialog showDisconnectDialog(Context context, 104 AlertDialog dialog, 105 DialogInterface.OnClickListener disconnectListener, 106 CharSequence title, CharSequence message) { 107 if (dialog == null) { 108 dialog = new AlertDialog.Builder(context) 109 .setPositiveButton(android.R.string.ok, disconnectListener) 110 .setNegativeButton(android.R.string.cancel, null) 111 .create(); 112 } else { 113 if (dialog.isShowing()) { 114 dialog.dismiss(); 115 } 116 // use disconnectListener for the correct profile(s) 117 CharSequence okText = context.getText(android.R.string.ok); 118 dialog.setButton(DialogInterface.BUTTON_POSITIVE, 119 okText, disconnectListener); 120 } 121 dialog.setTitle(title); 122 dialog.setMessage(message); 123 dialog.show(); 124 return dialog; 125 } 126 127 @VisibleForTesting showConnectingError(Context context, String name, LocalBluetoothManager manager)128 static void showConnectingError(Context context, String name, LocalBluetoothManager manager) { 129 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider().visible(context, 130 SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR, 131 0); 132 showError(context, name, R.string.bluetooth_connecting_error_message, manager); 133 } 134 showError(Context context, String name, int messageResId)135 static void showError(Context context, String name, int messageResId) { 136 showError(context, name, messageResId, getLocalBtManager(context)); 137 } 138 showError(Context context, String name, int messageResId, LocalBluetoothManager manager)139 private static void showError(Context context, String name, int messageResId, 140 LocalBluetoothManager manager) { 141 String message = context.getString(messageResId, name); 142 Context activity = manager.getForegroundActivity(); 143 if (manager.isForegroundActivity()) { 144 try { 145 new AlertDialog.Builder(activity) 146 .setTitle(R.string.bluetooth_error_title) 147 .setMessage(message) 148 .setPositiveButton(android.R.string.ok, null) 149 .show(); 150 } catch (Exception e) { 151 Log.e(TAG, "Cannot show error dialog.", e); 152 } 153 } else { 154 Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); 155 } 156 } 157 getLocalBtManager(Context context)158 public static LocalBluetoothManager getLocalBtManager(Context context) { 159 return LocalBluetoothManager.getInstance(context, mOnInitCallback); 160 } 161 162 /** 163 * Obtains a {@link LocalBluetoothManager}. 164 * 165 * To avoid StrictMode ThreadPolicy violation, will get it in another thread. 166 */ getLocalBluetoothManager(Context context)167 public static LocalBluetoothManager getLocalBluetoothManager(Context context) { 168 final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>( 169 // Avoid StrictMode ThreadPolicy violation 170 () -> getLocalBtManager(context)); 171 try { 172 localBtManagerFutureTask.run(); 173 return localBtManagerFutureTask.get(); 174 } catch (InterruptedException | ExecutionException e) { 175 Log.w(TAG, "Error getting LocalBluetoothManager.", e); 176 return null; 177 } 178 } 179 createRemoteName(Context context, BluetoothDevice device)180 public static String createRemoteName(Context context, BluetoothDevice device) { 181 String mRemoteName = device != null ? device.getAlias() : null; 182 183 if (mRemoteName == null) { 184 mRemoteName = context.getString(R.string.unknown); 185 } 186 return mRemoteName; 187 } 188 189 private static final ErrorListener mErrorListener = new ErrorListener() { 190 @Override 191 public void onShowError(Context context, String name, int messageResId) { 192 showError(context, name, messageResId); 193 } 194 }; 195 196 private static final BluetoothManagerCallback mOnInitCallback = new BluetoothManagerCallback() { 197 @Override 198 public void onBluetoothManagerInitialized(Context appContext, 199 LocalBluetoothManager bluetoothManager) { 200 BluetoothUtils.setErrorListener(mErrorListener); 201 } 202 }; 203 isBluetoothScanningEnabled(Context context)204 public static boolean isBluetoothScanningEnabled(Context context) { 205 return Settings.Global.getInt(context.getContentResolver(), 206 Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1; 207 } 208 209 /** 210 * Returns the Bluetooth Package name 211 */ findBluetoothPackageName(Context context)212 public static String findBluetoothPackageName(Context context) 213 throws NameNotFoundException { 214 // this activity will always be in the package where the rest of Bluetooth lives 215 final String sentinelActivity = "com.android.bluetooth.opp.BluetoothOppLauncherActivity"; 216 PackageManager packageManager = context.createContextAsUser(UserHandle.SYSTEM, 0) 217 .getPackageManager(); 218 String[] allPackages = packageManager.getPackagesForUid(BLUETOOTH_UID); 219 String matchedPackage = null; 220 for (String candidatePackage : allPackages) { 221 PackageInfo packageInfo; 222 try { 223 packageInfo = 224 packageManager.getPackageInfo( 225 candidatePackage, 226 PackageManager.GET_ACTIVITIES 227 | PackageManager.MATCH_ANY_USER 228 | PackageManager.MATCH_UNINSTALLED_PACKAGES 229 | PackageManager.MATCH_DISABLED_COMPONENTS); 230 } catch (NameNotFoundException e) { 231 // rethrow 232 throw e; 233 } 234 if (packageInfo.activities == null) { 235 continue; 236 } 237 for (ActivityInfo activity : packageInfo.activities) { 238 if (sentinelActivity.equals(activity.name)) { 239 if (matchedPackage == null) { 240 matchedPackage = candidatePackage; 241 } else { 242 throw new NameNotFoundException("multiple main bluetooth packages found"); 243 } 244 } 245 } 246 } 247 if (matchedPackage != null) { 248 return matchedPackage; 249 } 250 throw new NameNotFoundException("Could not find main bluetooth package"); 251 } 252 253 /** 254 * Returns all cachedBluetoothDevices with the same groupId. 255 * @param cachedBluetoothDevice The main cachedBluetoothDevice. 256 * @return all cachedBluetoothDevices with the same groupId. 257 */ findAllCachedBluetoothDevicesByGroupId( LocalBluetoothManager localBtMgr, CachedBluetoothDevice cachedBluetoothDevice)258 public static Set<CachedBluetoothDevice> findAllCachedBluetoothDevicesByGroupId( 259 LocalBluetoothManager localBtMgr, 260 CachedBluetoothDevice cachedBluetoothDevice) { 261 Set<CachedBluetoothDevice> cachedBluetoothDevices = new HashSet<>(); 262 if (cachedBluetoothDevice == null) { 263 Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: no cachedBluetoothDevice"); 264 return cachedBluetoothDevices; 265 } 266 int deviceGroupId = cachedBluetoothDevice.getGroupId(); 267 if (deviceGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 268 cachedBluetoothDevices.add(cachedBluetoothDevice); 269 return cachedBluetoothDevices; 270 } 271 272 if (localBtMgr == null) { 273 Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: no LocalBluetoothManager"); 274 return cachedBluetoothDevices; 275 } 276 CachedBluetoothDevice mainDevice = 277 localBtMgr.getCachedDeviceManager().getCachedDevicesCopy().stream() 278 .filter(cachedDevice -> cachedDevice.getGroupId() == deviceGroupId) 279 .findFirst().orElse(null); 280 if (mainDevice == null) { 281 Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: groupId = " + deviceGroupId 282 + ", no main device."); 283 return cachedBluetoothDevices; 284 } 285 cachedBluetoothDevice = mainDevice; 286 cachedBluetoothDevices.add(cachedBluetoothDevice); 287 cachedBluetoothDevices.addAll(cachedBluetoothDevice.getMemberDevice()); 288 Log.d(TAG, "findAllCachedBluetoothDevicesByGroupId: groupId = " + deviceGroupId 289 + " , cachedBluetoothDevice = " + cachedBluetoothDevice 290 + " , deviceList = " + cachedBluetoothDevices); 291 return cachedBluetoothDevices; 292 } 293 294 /** 295 * Preloads the values and run the Runnable afterwards. 296 * @param suppliers the value supplier, should be a memoized supplier 297 * @param runnable the runnable to be run after value is preloaded 298 */ preloadAndRun(List<Supplier<?>> suppliers, Runnable runnable)299 public static void preloadAndRun(List<Supplier<?>> suppliers, Runnable runnable) { 300 if (!Flags.enableOffloadBluetoothOperationsToBackgroundThread()) { 301 runnable.run(); 302 return; 303 } 304 ThreadUtils.postOnBackgroundThread(() -> { 305 for (Supplier<?> supplier : suppliers) { 306 Object unused = supplier.get(); 307 } 308 ThreadUtils.postOnMainThread(runnable); 309 }); 310 } 311 312 /** 313 * Check if need to block pairing during audio sharing 314 * 315 * @param localBtManager {@link LocalBluetoothManager} 316 * @return if need to block pairing during audio sharing 317 */ shouldBlockPairingInAudioSharing( @onNull LocalBluetoothManager localBtManager)318 public static boolean shouldBlockPairingInAudioSharing( 319 @NonNull LocalBluetoothManager localBtManager) { 320 if (!BluetoothUtils.isBroadcasting(localBtManager)) return false; 321 LocalBluetoothLeBroadcastAssistant assistant = 322 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 323 CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager(); 324 List<BluetoothDevice> connectedDevices = 325 assistant == null ? ImmutableList.of() : assistant.getAllConnectedDevices(); 326 Collection<CachedBluetoothDevice> bondedDevices = 327 deviceManager == null ? ImmutableList.of() : deviceManager.getCachedDevicesCopy(); 328 // Block the pairing if there is ongoing audio sharing session and 329 // a) there is already one temp bond sink bonded 330 // or b) there are already two sinks joining the audio sharing 331 return assistant != null && deviceManager != null 332 && (bondedDevices.stream().anyMatch( 333 d -> BluetoothUtils.isTemporaryBondDevice(d.getDevice()) 334 && d.getBondState() == BluetoothDevice.BOND_BONDED) 335 || connectedDevices.stream().filter( 336 d -> BluetoothUtils.hasActiveLocalBroadcastSourceForBtDevice(d, 337 localBtManager)) 338 .map(d -> BluetoothUtils.getGroupId(deviceManager.findDevice(d))).collect( 339 Collectors.toSet()).size() >= 2); 340 } 341 342 /** 343 * Show block pairing dialog during audio sharing 344 * @param context The dialog context 345 * @param dialog The dialog if already exists 346 * @param localBtManager {@link LocalBluetoothManager} 347 * @return The block pairing dialog 348 */ 349 @Nullable showBlockPairingDialog(@onNull Context context, @Nullable AlertDialog dialog, @Nullable LocalBluetoothManager localBtManager)350 static AlertDialog showBlockPairingDialog(@NonNull Context context, 351 @Nullable AlertDialog dialog, @Nullable LocalBluetoothManager localBtManager) { 352 if (!com.android.settingslib.flags.Flags.enableTemporaryBondDevicesUi()) return null; 353 if (dialog != null && dialog.isShowing()) return dialog; 354 if (dialog == null) { 355 AlertDialog.Builder builder = new AlertDialog.Builder(context) 356 .setNegativeButton(android.R.string.cancel, null) 357 .setTitle(R.string.audio_sharing_block_pairing_dialog_title) 358 .setMessage(R.string.audio_sharing_block_pairing_dialog_content); 359 LocalBluetoothLeBroadcast broadcast = localBtManager == null ? null : 360 localBtManager.getProfileManager().getLeAudioBroadcastProfile(); 361 if (broadcast != null) { 362 builder.setPositiveButton(R.string.audio_sharing_turn_off_button_label, 363 (dlg, which) -> broadcast.stopLatestBroadcast()); 364 } 365 dialog = builder.create(); 366 } 367 dialog.show(); 368 return dialog; 369 } 370 371 /** Enables/disables LE Audio profile for the device. */ setLeAudioEnabled( @onNull LocalBluetoothManager manager, @NonNull CachedBluetoothDevice cachedDevice, boolean enable)372 public static void setLeAudioEnabled( 373 @NonNull LocalBluetoothManager manager, 374 @NonNull CachedBluetoothDevice cachedDevice, 375 boolean enable) { 376 List<CachedBluetoothDevice> devices = 377 List.copyOf(findAllCachedBluetoothDevicesByGroupId(manager, cachedDevice)); 378 setLeAudioEnabled(manager, devices, enable); 379 } 380 381 /** Enables/disables LE Audio profile for the devices in the same csip group. */ setLeAudioEnabled( @onNull LocalBluetoothManager manager, @NonNull List<CachedBluetoothDevice> devicesWithSameGroupId, boolean enable)382 public static void setLeAudioEnabled( 383 @NonNull LocalBluetoothManager manager, 384 @NonNull List<CachedBluetoothDevice> devicesWithSameGroupId, 385 boolean enable) { 386 LocalBluetoothProfileManager profileManager = manager.getProfileManager(); 387 LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); 388 List<CachedBluetoothDevice> leAudioDevices = 389 getDevicesWithProfile(devicesWithSameGroupId, leAudioProfile); 390 if (leAudioDevices.isEmpty()) { 391 Log.i(TAG, "Fail to setLeAudioEnabled, no LE Audio profile found."); 392 } 393 boolean dualModeEnabled = SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false); 394 395 if (enable && !dualModeEnabled) { 396 Log.i(TAG, "Disabling classic audio profiles because dual mode is disabled"); 397 setProfileEnabledWhenChangingLeAudio( 398 devicesWithSameGroupId, profileManager.getA2dpProfile(), false); 399 setProfileEnabledWhenChangingLeAudio( 400 devicesWithSameGroupId, profileManager.getHeadsetProfile(), false); 401 } 402 403 HearingAidProfile asha = profileManager.getHearingAidProfile(); 404 LocalBluetoothLeBroadcastAssistant broadcastAssistant = 405 profileManager.getLeAudioBroadcastAssistantProfile(); 406 407 for (CachedBluetoothDevice leAudioDevice : leAudioDevices) { 408 Log.d( 409 TAG, 410 "device:" 411 + leAudioDevice.getDevice().getAnonymizedAddress() 412 + " set LE profile enabled: " 413 + enable); 414 leAudioProfile.setEnabled(leAudioDevice.getDevice(), enable); 415 if (asha != null) { 416 asha.setEnabled(leAudioDevice.getDevice(), !enable); 417 } 418 if (broadcastAssistant != null) { 419 Log.d( 420 TAG, 421 "device:" 422 + leAudioDevice.getDevice().getAnonymizedAddress() 423 + " enable LE broadcast assistant profile: " 424 + enable); 425 broadcastAssistant.setEnabled(leAudioDevice.getDevice(), enable); 426 } 427 } 428 429 if (!enable && !dualModeEnabled) { 430 Log.i(TAG, "Enabling classic audio profiles because dual mode is disabled"); 431 setProfileEnabledWhenChangingLeAudio( 432 devicesWithSameGroupId, profileManager.getA2dpProfile(), true); 433 setProfileEnabledWhenChangingLeAudio( 434 devicesWithSameGroupId, profileManager.getHeadsetProfile(), true); 435 } 436 } 437 getDevicesWithProfile( List<CachedBluetoothDevice> devices, LocalBluetoothProfile profile)438 private static List<CachedBluetoothDevice> getDevicesWithProfile( 439 List<CachedBluetoothDevice> devices, LocalBluetoothProfile profile) { 440 List<CachedBluetoothDevice> devicesWithProfile = new ArrayList<>(); 441 for (CachedBluetoothDevice device : devices) { 442 for (LocalBluetoothProfile currentProfile : device.getProfiles()) { 443 if (currentProfile.toString().equals(profile.toString())) { 444 devicesWithProfile.add(device); 445 } 446 } 447 } 448 return devicesWithProfile; 449 } 450 setProfileEnabledWhenChangingLeAudio( List<CachedBluetoothDevice> devices, @Nullable LocalBluetoothProfile profile, boolean enable)451 private static void setProfileEnabledWhenChangingLeAudio( 452 List<CachedBluetoothDevice> devices, 453 @Nullable LocalBluetoothProfile profile, 454 boolean enable) { 455 if (profile == null) { 456 Log.i(TAG, "profile is null"); 457 return; 458 } 459 List<CachedBluetoothDevice> deviceWithProfile = getDevicesWithProfile(devices, profile); 460 Log.d(TAG, "Set " + profile + " enabled:" + enable + " when switching LE Audio"); 461 for (CachedBluetoothDevice profileDevice : deviceWithProfile) { 462 if (profile.isEnabled(profileDevice.getDevice()) != enable) { 463 Log.d( 464 TAG, 465 "The " 466 + profileDevice.getDevice().getAnonymizedAddress() 467 + ":" 468 + profile 469 + " set to " 470 + enable); 471 profile.setEnabled(profileDevice.getDevice(), enable); 472 } else { 473 Log.d( 474 TAG, 475 "The " 476 + profileDevice.getDevice().getAnonymizedAddress() 477 + ":" 478 + profile 479 + " profile is already " 480 + enable 481 + ". Do nothing."); 482 } 483 } 484 } 485 } 486