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