blob: d87c4c59e0d59ea797333699433200442f4f36b1 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.settings.bluetooth;
import android.annotation.NonNull;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserManager;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.R;
import com.android.car.settings.common.Logger;
import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
/**
* This {@link Activity} handles requests to toggle Bluetooth by collecting user
* consent and waiting until the state change is completed. It can also be used to make the device
* explicitly discoverable for a given amount of time.
*/
public class BluetoothRequestPermissionActivity extends Activity {
private static final Logger LOG = new Logger(BluetoothRequestPermissionActivity.class);
@VisibleForTesting
static final int REQUEST_UNKNOWN = 0;
@VisibleForTesting
static final int REQUEST_ENABLE = 1;
@VisibleForTesting
static final int REQUEST_DISABLE = 2;
@VisibleForTesting
static final int REQUEST_ENABLE_DISCOVERABLE = 3;
private static final int DISCOVERABLE_TIMEOUT_TWO_MINUTES = 120;
private static final int DISCOVERABLE_TIMEOUT_ONE_HOUR = 3600;
@VisibleForTesting
static final int DEFAULT_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_TWO_MINUTES;
@VisibleForTesting
static final int MAX_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_ONE_HOUR;
private AlertDialog mDialog;
private int mRequest;
private int mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT;
@NonNull
private CharSequence mAppLabel;
private LocalBluetoothAdapter mLocalBluetoothAdapter;
private LocalBluetoothManager mLocalBluetoothManager;
private StateChangeReceiver mReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRequest = parseIntent();
if (mRequest == REQUEST_UNKNOWN) {
finishWithResult(RESULT_CANCELED);
return;
}
mLocalBluetoothManager = LocalBluetoothManager.getInstance(
getApplicationContext(), /* onInitCallback= */ null);
if (mLocalBluetoothManager == null) {
LOG.e("Bluetooth is not supported on this device");
finishWithResult(RESULT_CANCELED);
}
mLocalBluetoothAdapter = mLocalBluetoothManager.getBluetoothAdapter();
int btState = mLocalBluetoothAdapter.getState();
switch (mRequest) {
case REQUEST_DISABLE:
switch (btState) {
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
proceedAndFinish();
break;
case BluetoothAdapter.STATE_ON:
case BluetoothAdapter.STATE_TURNING_ON:
mDialog = createRequestDisableBluetoothDialog();
mDialog.show();
break;
default:
LOG.e("Unknown adapter state: " + btState);
finishWithResult(RESULT_CANCELED);
break;
}
break;
case REQUEST_ENABLE:
switch (btState) {
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
mDialog = createRequestEnableBluetoothDialog();
mDialog.show();
break;
case BluetoothAdapter.STATE_ON:
case BluetoothAdapter.STATE_TURNING_ON:
proceedAndFinish();
break;
default:
LOG.e("Unknown adapter state: " + btState);
finishWithResult(RESULT_CANCELED);
break;
}
break;
case REQUEST_ENABLE_DISCOVERABLE:
switch (btState) {
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
case BluetoothAdapter.STATE_TURNING_ON:
/*
* Strictly speaking STATE_TURNING_ON belong with STATE_ON; however, BT
* may not be ready when the user clicks yes and we would fail to turn on
* discovery mode. We still show the dialog and handle this case via the
* broadcast receiver.
*/
mDialog = createRequestEnableBluetoothDialogWithTimeout(mTimeout);
mDialog.show();
break;
case BluetoothAdapter.STATE_ON:
mDialog = createDiscoverableConfirmDialog(mTimeout);
mDialog.show();
break;
default:
LOG.e("Unknown adapter state: " + btState);
finishWithResult(RESULT_CANCELED);
break;
}
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
}
private void proceedAndFinish() {
if (mRequest == REQUEST_ENABLE_DISCOVERABLE) {
finishWithResult(setDiscoverable(mTimeout));
} else {
finishWithResult(RESULT_OK);
}
}
// Returns the code that should be used to finish the activity.
private int setDiscoverable(int timeoutSeconds) {
if (!mLocalBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,
timeoutSeconds)) {
return RESULT_CANCELED;
}
// If already in discoverable mode, this will extend the timeout.
long endTime = System.currentTimeMillis() + (long) timeoutSeconds * 1000;
BluetoothUtils.persistDiscoverableEndTimestamp(/* context= */ this, endTime);
if (timeoutSeconds > 0) {
BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(/* context= */ this, endTime);
}
int returnCode = timeoutSeconds;
return returnCode < RESULT_FIRST_USER ? RESULT_FIRST_USER : returnCode;
}
private void finishWithResult(int result) {
if (mDialog != null) {
mDialog.dismiss();
}
setResult(result);
finish();
}
private int parseIntent() {
int request;
Intent intent = getIntent();
if (intent == null) {
return REQUEST_UNKNOWN;
}
switch (intent.getAction()) {
case BluetoothAdapter.ACTION_REQUEST_ENABLE:
request = REQUEST_ENABLE;
break;
case BluetoothAdapter.ACTION_REQUEST_DISABLE:
request = REQUEST_DISABLE;
break;
case BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE:
request = REQUEST_ENABLE_DISCOVERABLE;
mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,
DEFAULT_DISCOVERABLE_TIMEOUT);
if (mTimeout < 1 || mTimeout > MAX_DISCOVERABLE_TIMEOUT) {
mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT;
}
break;
default:
LOG.e("Error: this activity may be started only with intent "
+ BluetoothAdapter.ACTION_REQUEST_ENABLE);
return REQUEST_UNKNOWN;
}
String packageName = getCallingPackage();
if (TextUtils.isEmpty(packageName)) {
packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
}
if (!TextUtils.isEmpty(packageName)) {
try {
ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
packageName, 0);
mAppLabel = applicationInfo.loadLabel(getPackageManager());
} catch (PackageManager.NameNotFoundException e) {
LOG.e("Couldn't find app with package name " + packageName);
return REQUEST_UNKNOWN;
}
}
return request;
}
private AlertDialog createWaitingDialog() {
int message = mRequest == REQUEST_DISABLE ? R.string.bluetooth_turning_off
: R.string.bluetooth_turning_on;
return new AlertDialog.Builder(/* context= */ this)
.setMessage(message)
.setCancelable(false).setOnCancelListener(
dialog -> finishWithResult(RESULT_CANCELED))
.create();
}
// Assumes {@code timeoutSeconds} > 0.
private AlertDialog createDiscoverableConfirmDialog(int timeoutSeconds) {
String message = mAppLabel != null
? getString(R.string.bluetooth_ask_discovery, mAppLabel, timeoutSeconds)
: getString(R.string.bluetooth_ask_discovery_no_name, timeoutSeconds);
return new AlertDialog.Builder(/* context= */ this)
.setMessage(message)
.setPositiveButton(R.string.allow, (dialog, which) -> proceedAndFinish())
.setNegativeButton(R.string.deny,
(dialog, which) -> finishWithResult(RESULT_CANCELED))
.setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
.create();
}
private AlertDialog createRequestEnableBluetoothDialog() {
String message = mAppLabel != null
? getString(R.string.bluetooth_ask_enablement, mAppLabel)
: getString(R.string.bluetooth_ask_enablement_no_name);
return new AlertDialog.Builder(/* context= */ this)
.setMessage(message)
.setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth)
.setNegativeButton(R.string.deny,
(dialog, which) -> finishWithResult(RESULT_CANCELED))
.setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
.create();
}
// Assumes {@code timeoutSeconds} > 0.
private AlertDialog createRequestEnableBluetoothDialogWithTimeout(int timeoutSeconds) {
String message = mAppLabel != null
? getString(R.string.bluetooth_ask_enablement_and_discovery, mAppLabel,
timeoutSeconds)
: getString(R.string.bluetooth_ask_enablement_and_discovery_no_name,
timeoutSeconds);
return new AlertDialog.Builder(/* context= */ this)
.setMessage(message)
.setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth)
.setNegativeButton(R.string.deny,
(dialog, which) -> finishWithResult(RESULT_CANCELED))
.setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
.create();
}
private void onConfirmEnableBluetooth(DialogInterface dialog, int which) {
UserManager userManager = getSystemService(UserManager.class);
if (userManager.hasUserRestriction(UserManager.DISALLOW_BLUETOOTH)) {
// If Bluetooth is disallowed, don't try to enable it, show policy
// transparency message instead.
DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
Intent intent = dpm.createAdminSupportIntent(
UserManager.DISALLOW_BLUETOOTH);
if (intent != null) {
startActivity(intent);
}
return;
}
mLocalBluetoothAdapter.enable();
int desiredState = BluetoothAdapter.STATE_ON;
if (mLocalBluetoothAdapter.getState() == desiredState) {
proceedAndFinish();
} else {
// Register this receiver to listen for state change after the enabling has started.
mReceiver = new StateChangeReceiver(desiredState);
registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
if (mRequest == REQUEST_ENABLE) {
// Show dialog while waiting for enabling to complete.
mDialog = createWaitingDialog();
} else {
mDialog = createDiscoverableConfirmDialog(mTimeout);
}
mDialog.show();
}
}
private AlertDialog createRequestDisableBluetoothDialog() {
String message = mAppLabel != null
? getString(R.string.bluetooth_ask_disablement, mAppLabel)
: getString(R.string.bluetooth_ask_disablement_no_name);
return new AlertDialog.Builder(/* context= */ this)
.setMessage(message)
.setPositiveButton(R.string.allow, this::onConfirmDisableBluetooth)
.setNegativeButton(R.string.deny,
(dialog, which) -> finishWithResult(RESULT_CANCELED))
.setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
.create();
}
private void onConfirmDisableBluetooth(DialogInterface dialog, int which) {
mLocalBluetoothAdapter.disable();
int desiredState = BluetoothAdapter.STATE_OFF;
if (mLocalBluetoothAdapter.getState() == desiredState) {
proceedAndFinish();
} else {
// Register this receiver to listen for state change after the disabling has started.
mReceiver = new StateChangeReceiver(desiredState);
registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
// Show dialog while waiting for disabling to complete.
mDialog = createWaitingDialog();
mDialog.show();
}
}
@VisibleForTesting
int getRequestType() {
return mRequest;
}
@VisibleForTesting
int getTimeout() {
return mTimeout;
}
@VisibleForTesting
AlertDialog getCurrentDialog() {
return mDialog;
}
/**
* Listens for bluetooth state changes and finishes the activity if changed to the desired
* state. If the desired bluetooth state is not received in time, the activity is finished with
* {@link Activity#RESULT_CANCELED}.
*/
private final class StateChangeReceiver extends BroadcastReceiver {
private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec
private final int mDesiredState;
StateChangeReceiver(int desiredState) {
mDesiredState = desiredState;
getWindow().getDecorView().postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
finishWithResult(RESULT_CANCELED);
}
}, TOGGLE_TIMEOUT_MILLIS);
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
int currentState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothDevice.ERROR);
if (mDesiredState == currentState) {
proceedAndFinish();
}
}
}
}