1 /* 2 * Copyright (C) 2019 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.car.settings.bluetooth; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Activity; 22 import android.app.AlertDialog; 23 import android.app.admin.DevicePolicyManager; 24 import android.bluetooth.BluetoothAdapter; 25 import android.bluetooth.BluetoothDevice; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ResolveInfo; 34 import android.os.Bundle; 35 import android.os.UserManager; 36 import android.text.TextUtils; 37 38 import androidx.activity.ComponentActivity; 39 import androidx.annotation.VisibleForTesting; 40 41 import com.android.car.settings.R; 42 import com.android.car.settings.common.Logger; 43 import com.android.car.ui.AlertDialogBuilder; 44 import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver; 45 import com.android.settingslib.bluetooth.LocalBluetoothAdapter; 46 import com.android.settingslib.bluetooth.LocalBluetoothManager; 47 import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; 48 49 import java.util.List; 50 51 /** 52 * This {@link Activity} handles requests to toggle Bluetooth by collecting user 53 * consent and waiting until the state change is completed. It can also be used to make the device 54 * explicitly discoverable for a given amount of time. 55 */ 56 public class BluetoothRequestPermissionActivity extends ComponentActivity { 57 private static final Logger LOG = new Logger(BluetoothRequestPermissionActivity.class); 58 59 @VisibleForTesting 60 static final int REQUEST_UNKNOWN = 0; 61 @VisibleForTesting 62 static final int REQUEST_ENABLE = 1; 63 @VisibleForTesting 64 static final int REQUEST_DISABLE = 2; 65 @VisibleForTesting 66 static final int REQUEST_ENABLE_DISCOVERABLE = 3; 67 68 private static final int DISCOVERABLE_TIMEOUT_TWO_MINUTES = 120; 69 private static final int DISCOVERABLE_TIMEOUT_ONE_HOUR = 3600; 70 71 @VisibleForTesting 72 static final String EXTRA_BYPASS_CONFIRM_DIALOG = "bypassConfirmDialog"; 73 74 @VisibleForTesting 75 static final int DEFAULT_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_TWO_MINUTES; 76 @VisibleForTesting 77 static final int MAX_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_ONE_HOUR; 78 79 private AlertDialog mDialog; 80 private boolean mBypassConfirmDialog = false; 81 private int mRequest; 82 private int mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT; 83 84 @NonNull 85 private CharSequence mAppLabel; 86 private LocalBluetoothAdapter mLocalBluetoothAdapter; 87 private LocalBluetoothManager mLocalBluetoothManager; 88 private StateChangeReceiver mReceiver; 89 90 @Override onCreate(Bundle savedInstanceState)91 protected void onCreate(Bundle savedInstanceState) { 92 super.onCreate(savedInstanceState); 93 94 getLifecycle().addObserver(new HideNonSystemOverlayMixin(this)); 95 96 mRequest = parseIntent(); 97 if (mRequest == REQUEST_UNKNOWN) { 98 finishWithResult(RESULT_CANCELED); 99 return; 100 } 101 102 mLocalBluetoothManager = LocalBluetoothManager.getInstance( 103 getApplicationContext(), /* onInitCallback= */ null); 104 if (mLocalBluetoothManager == null) { 105 LOG.e("Bluetooth is not supported on this device"); 106 finishWithResult(RESULT_CANCELED); 107 } 108 109 mLocalBluetoothAdapter = mLocalBluetoothManager.getBluetoothAdapter(); 110 111 int btState = mLocalBluetoothAdapter.getState(); 112 switch (mRequest) { 113 case REQUEST_DISABLE: 114 switch (btState) { 115 case BluetoothAdapter.STATE_OFF: 116 case BluetoothAdapter.STATE_TURNING_OFF: 117 proceedAndFinish(); 118 break; 119 120 case BluetoothAdapter.STATE_ON: 121 case BluetoothAdapter.STATE_TURNING_ON: 122 mDialog = createRequestDisableBluetoothDialog(); 123 mDialog.show(); 124 break; 125 126 default: 127 LOG.e("Unknown adapter state: " + btState); 128 finishWithResult(RESULT_CANCELED); 129 break; 130 } 131 break; 132 case REQUEST_ENABLE: 133 switch (btState) { 134 case BluetoothAdapter.STATE_OFF: 135 case BluetoothAdapter.STATE_TURNING_OFF: 136 mDialog = createRequestEnableBluetoothDialog(); 137 mDialog.show(); 138 break; 139 case BluetoothAdapter.STATE_ON: 140 case BluetoothAdapter.STATE_TURNING_ON: 141 proceedAndFinish(); 142 break; 143 default: 144 LOG.e("Unknown adapter state: " + btState); 145 finishWithResult(RESULT_CANCELED); 146 break; 147 } 148 break; 149 case REQUEST_ENABLE_DISCOVERABLE: 150 switch (btState) { 151 case BluetoothAdapter.STATE_OFF: 152 case BluetoothAdapter.STATE_TURNING_OFF: 153 case BluetoothAdapter.STATE_TURNING_ON: 154 /* 155 * Strictly speaking STATE_TURNING_ON belong with STATE_ON; however, BT 156 * may not be ready when the user clicks yes and we would fail to turn on 157 * discovery mode. We still show the dialog and handle this case via the 158 * broadcast receiver. 159 */ 160 if (isSetupWizardDialogBypass()) { 161 /* 162 * In some cases, users may get to the setup wizard's bluetooth fragment 163 * while in this state. We still need to wait until we reach STATE_ON 164 * before enabling discovery mode but without showing a dialog. 165 */ 166 enableBluetoothWithWaitingDialog(/* dialogToShowOnWait= */ null); 167 } else { 168 mDialog = createRequestEnableBluetoothDialogWithTimeout(mTimeout); 169 mDialog.show(); 170 } 171 break; 172 case BluetoothAdapter.STATE_ON: 173 // Allow SetupWizard specifically to skip the discoverability dialog. 174 if (isSetupWizardDialogBypass()) { 175 proceedAndFinish(); 176 } else { 177 mDialog = createDiscoverableConfirmDialog(mTimeout); 178 mDialog.show(); 179 } 180 break; 181 default: 182 LOG.e("Unknown adapter state: " + btState); 183 finishWithResult(RESULT_CANCELED); 184 break; 185 } 186 break; 187 } 188 } 189 190 @Override onDestroy()191 protected void onDestroy() { 192 super.onDestroy(); 193 if (mReceiver != null) { 194 unregisterReceiver(mReceiver); 195 } 196 } 197 isSetupWizardDialogBypass()198 private boolean isSetupWizardDialogBypass() { 199 String callerName = getCallingPackage(); 200 return mBypassConfirmDialog && callerName != null 201 && callerName.equals(getSetupWizardPackageName()); 202 } 203 204 @Nullable getSetupWizardPackageName()205 private String getSetupWizardPackageName() { 206 Intent intent = new Intent(Intent.ACTION_MAIN); 207 intent.addCategory(Intent.CATEGORY_SETUP_WIZARD); 208 209 List<ResolveInfo> matches = getPackageManager().queryIntentActivities(intent, 210 PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE 211 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 212 | PackageManager.MATCH_DISABLED_COMPONENTS); 213 if (matches.size() == 1) { 214 return matches.get(0).getComponentInfo().packageName; 215 } else { 216 LOG.e("There should probably be exactly one setup wizard; found " + matches.size() 217 + ": matches=" + matches); 218 return null; 219 } 220 } 221 proceedAndFinish()222 private void proceedAndFinish() { 223 if (mRequest == REQUEST_ENABLE_DISCOVERABLE) { 224 finishWithResult(setDiscoverable(mTimeout)); 225 } else { 226 finishWithResult(RESULT_OK); 227 } 228 } 229 230 // Returns the code that should be used to finish the activity. setDiscoverable(int timeoutSeconds)231 private int setDiscoverable(int timeoutSeconds) { 232 if (!mLocalBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, 233 timeoutSeconds)) { 234 return RESULT_CANCELED; 235 } 236 237 // If already in discoverable mode, this will extend the timeout. 238 long endTime = System.currentTimeMillis() + (long) timeoutSeconds * 1000; 239 BluetoothUtils.persistDiscoverableEndTimestamp(/* context= */ this, endTime); 240 if (timeoutSeconds > 0) { 241 BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(/* context= */ this, endTime); 242 } 243 244 int returnCode = timeoutSeconds; 245 return returnCode < RESULT_FIRST_USER ? RESULT_FIRST_USER : returnCode; 246 } 247 finishWithResult(int result)248 private void finishWithResult(int result) { 249 if (mDialog != null) { 250 mDialog.dismiss(); 251 } 252 setResult(result); 253 finish(); 254 } 255 parseIntent()256 private int parseIntent() { 257 int request; 258 Intent intent = getIntent(); 259 if (intent == null) { 260 return REQUEST_UNKNOWN; 261 } 262 263 switch (intent.getAction()) { 264 case BluetoothAdapter.ACTION_REQUEST_ENABLE: 265 request = REQUEST_ENABLE; 266 break; 267 case BluetoothAdapter.ACTION_REQUEST_DISABLE: 268 request = REQUEST_DISABLE; 269 break; 270 case BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE: 271 request = REQUEST_ENABLE_DISCOVERABLE; 272 mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 273 DEFAULT_DISCOVERABLE_TIMEOUT); 274 mBypassConfirmDialog = intent.getBooleanExtra(EXTRA_BYPASS_CONFIRM_DIALOG, false); 275 276 if (mTimeout < 1 || mTimeout > MAX_DISCOVERABLE_TIMEOUT) { 277 mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT; 278 } 279 break; 280 default: 281 LOG.e("Error: this activity may be started only with intent " 282 + BluetoothAdapter.ACTION_REQUEST_ENABLE); 283 return REQUEST_UNKNOWN; 284 } 285 286 String packageName = getCallingPackage(); 287 if (TextUtils.isEmpty(packageName)) { 288 packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); 289 } 290 if (!mBypassConfirmDialog && !TextUtils.isEmpty(packageName)) { 291 try { 292 ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo( 293 packageName, 0); 294 mAppLabel = applicationInfo.loadLabel(getPackageManager()); 295 } catch (PackageManager.NameNotFoundException e) { 296 LOG.e("Couldn't find app with package name " + packageName); 297 return REQUEST_UNKNOWN; 298 } 299 } 300 301 return request; 302 } 303 createWaitingDialog()304 private AlertDialog createWaitingDialog() { 305 int message = mRequest == REQUEST_DISABLE ? R.string.bluetooth_turning_off 306 : R.string.bluetooth_turning_on; 307 308 return new AlertDialogBuilder(/* context= */ this) 309 .setMessage(message) 310 .setCancelable(false).setOnCancelListener( 311 dialog -> finishWithResult(RESULT_CANCELED)) 312 .create(); 313 } 314 315 // Assumes {@code timeoutSeconds} > 0. createDiscoverableConfirmDialog(int timeoutSeconds)316 private AlertDialog createDiscoverableConfirmDialog(int timeoutSeconds) { 317 String message = mAppLabel != null 318 ? getString(R.string.bluetooth_ask_discovery, mAppLabel, timeoutSeconds) 319 : getString(R.string.bluetooth_ask_discovery_no_name, timeoutSeconds); 320 321 return new AlertDialogBuilder(/* context= */ this) 322 .setMessage(message) 323 .setPositiveButton(R.string.allow, (dialog, which) -> proceedAndFinish()) 324 .setNegativeButton(R.string.deny, 325 (dialog, which) -> finishWithResult(RESULT_CANCELED)) 326 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED)) 327 .create(); 328 } 329 createRequestEnableBluetoothDialog()330 private AlertDialog createRequestEnableBluetoothDialog() { 331 String message = mAppLabel != null 332 ? getString(R.string.bluetooth_ask_enablement, mAppLabel) 333 : getString(R.string.bluetooth_ask_enablement_no_name); 334 335 return new AlertDialogBuilder(/* context= */ this) 336 .setMessage(message) 337 .setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth) 338 .setNegativeButton(R.string.deny, 339 (dialog, which) -> finishWithResult(RESULT_CANCELED)) 340 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED)) 341 .create(); 342 } 343 344 // Assumes {@code timeoutSeconds} > 0. createRequestEnableBluetoothDialogWithTimeout(int timeoutSeconds)345 private AlertDialog createRequestEnableBluetoothDialogWithTimeout(int timeoutSeconds) { 346 String message = mAppLabel != null 347 ? getString(R.string.bluetooth_ask_enablement_and_discovery, mAppLabel, 348 timeoutSeconds) 349 : getString(R.string.bluetooth_ask_enablement_and_discovery_no_name, 350 timeoutSeconds); 351 352 return new AlertDialogBuilder(/* context= */ this) 353 .setMessage(message) 354 .setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth) 355 .setNegativeButton(R.string.deny, 356 (dialog, which) -> finishWithResult(RESULT_CANCELED)) 357 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED)) 358 .create(); 359 } 360 onConfirmEnableBluetooth(DialogInterface dialog, int which)361 private void onConfirmEnableBluetooth(DialogInterface dialog, int which) { 362 UserManager userManager = getSystemService(UserManager.class); 363 if (userManager.hasUserRestriction(UserManager.DISALLOW_BLUETOOTH)) { 364 // If Bluetooth is disallowed, don't try to enable it, show policy 365 // transparency message instead. 366 DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class); 367 Intent intent = dpm.createAdminSupportIntent( 368 UserManager.DISALLOW_BLUETOOTH); 369 if (intent != null) { 370 startActivity(intent); 371 } 372 return; 373 } 374 375 if (mRequest == REQUEST_ENABLE) { 376 enableBluetoothWithWaitingDialog(createWaitingDialog()); 377 } else { 378 enableBluetoothWithWaitingDialog(createDiscoverableConfirmDialog(mTimeout)); 379 } 380 } 381 382 /* 383 * Ensure bluetooth is enabled and then check if it is in STATE_ON. If it isn't, register 384 * the broadcast receiver to wait for the state to change and show a waiting dialog if provided. 385 */ enableBluetoothWithWaitingDialog(@ullable AlertDialog dialogToShowOnWait)386 private void enableBluetoothWithWaitingDialog(@Nullable AlertDialog dialogToShowOnWait) { 387 mLocalBluetoothAdapter.enable(); 388 389 int desiredState = BluetoothAdapter.STATE_ON; 390 if (mLocalBluetoothAdapter.getState() == desiredState) { 391 proceedAndFinish(); 392 } else { 393 // Register this receiver to listen for state change after the enabling has started. 394 mReceiver = new StateChangeReceiver(desiredState); 395 registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); 396 397 if (dialogToShowOnWait != null) { 398 mDialog = dialogToShowOnWait; 399 mDialog.show(); 400 } 401 } 402 } 403 createRequestDisableBluetoothDialog()404 private AlertDialog createRequestDisableBluetoothDialog() { 405 String message = mAppLabel != null 406 ? getString(R.string.bluetooth_ask_disablement, mAppLabel) 407 : getString(R.string.bluetooth_ask_disablement_no_name); 408 409 return new AlertDialogBuilder(/* context= */ this) 410 .setMessage(message) 411 .setPositiveButton(R.string.allow, this::onConfirmDisableBluetooth) 412 .setNegativeButton(R.string.deny, 413 (dialog, which) -> finishWithResult(RESULT_CANCELED)) 414 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED)) 415 .create(); 416 } 417 onConfirmDisableBluetooth(DialogInterface dialog, int which)418 private void onConfirmDisableBluetooth(DialogInterface dialog, int which) { 419 mLocalBluetoothAdapter.disable(); 420 421 int desiredState = BluetoothAdapter.STATE_OFF; 422 if (mLocalBluetoothAdapter.getState() == desiredState) { 423 proceedAndFinish(); 424 } else { 425 // Register this receiver to listen for state change after the disabling has started. 426 mReceiver = new StateChangeReceiver(desiredState); 427 registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); 428 429 // Show dialog while waiting for disabling to complete. 430 mDialog = createWaitingDialog(); 431 mDialog.show(); 432 } 433 } 434 435 @VisibleForTesting getRequestType()436 int getRequestType() { 437 return mRequest; 438 } 439 440 @VisibleForTesting getTimeout()441 int getTimeout() { 442 return mTimeout; 443 } 444 445 @VisibleForTesting getCurrentDialog()446 AlertDialog getCurrentDialog() { 447 return mDialog; 448 } 449 450 @VisibleForTesting getCurrentReceiver()451 StateChangeReceiver getCurrentReceiver() { 452 return mReceiver; 453 } 454 455 /** 456 * Listens for bluetooth state changes and finishes the activity if changed to the desired 457 * state. If the desired bluetooth state is not received in time, the activity is finished with 458 * {@link Activity#RESULT_CANCELED}. 459 */ 460 @VisibleForTesting 461 final class StateChangeReceiver extends BroadcastReceiver { 462 private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec 463 private final int mDesiredState; 464 StateChangeReceiver(int desiredState)465 StateChangeReceiver(int desiredState) { 466 mDesiredState = desiredState; 467 468 getWindow().getDecorView().postDelayed(() -> { 469 if (!isFinishing() && !isDestroyed()) { 470 finishWithResult(RESULT_CANCELED); 471 } 472 }, TOGGLE_TIMEOUT_MILLIS); 473 } 474 475 @Override onReceive(Context context, Intent intent)476 public void onReceive(Context context, Intent intent) { 477 if (intent == null) { 478 return; 479 } 480 481 int currentState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 482 BluetoothDevice.ERROR); 483 if (mDesiredState == currentState) { 484 proceedAndFinish(); 485 } 486 } 487 } 488 } 489