diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 100a071483e..6aad2afc282 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2955,13 +2955,6 @@ - - - { + onDisableConfirmed(); + return Unit.INSTANCE; + }, + () -> { + cancelAndFinish(); + return Unit.INSTANCE; + }); } break; default: { @@ -126,18 +131,17 @@ public class RequestPermissionActivity extends Activity implements * case via the broadcast receiver. */ - /* - * Start the helper activity to: - * 1) ask the user about enabling bt AND discovery - * 2) enable BT upon confirmation - */ - Intent intent = new Intent(this, RequestPermissionHelperActivity.class); - intent.setAction(RequestPermissionHelperActivity.ACTION_INTERNAL_REQUEST_BT_ON); - intent.putExtra(RequestPermissionHelperActivity.EXTRA_APP_LABEL, mAppLabel); - if (mRequest == REQUEST_ENABLE_DISCOVERABLE) { - intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, mTimeout); - } - startActivityForResult(intent, 0); + // Start the helper activity to ask the user about enabling bt AND discovery + RequestPermissionHelper.INSTANCE.requestEnable(this, mAppLabel, + mRequest == REQUEST_ENABLE_DISCOVERABLE ? mTimeout : -1, + () -> { + onEnableConfirmed(); + return Unit.INSTANCE; + }, + () -> { + cancelAndFinish(); + return Unit.INSTANCE; + }); } break; case BluetoothAdapter.STATE_ON: { @@ -202,42 +206,27 @@ public class RequestPermissionActivity extends Activity implements mDialog.show(); } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - cancelAndFinish(); - return; + private void onEnableConfirmed() { + mBluetoothAdapter.enable(); + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { + proceedAndFinish(); + } else { + // If BT is not up yet, show "Turning on Bluetooth..." + mReceiver = new StateChangeReceiver(); + registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + createDialog(); } + } - switch (mRequest) { - case REQUEST_ENABLE: - case REQUEST_ENABLE_DISCOVERABLE: { - if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { - proceedAndFinish(); - } else { - // If BT is not up yet, show "Turning on Bluetooth..." - mReceiver = new StateChangeReceiver(); - registerReceiver(mReceiver, new IntentFilter( - BluetoothAdapter.ACTION_STATE_CHANGED)); - createDialog(); - } - } break; - - case REQUEST_DISABLE: { - if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { - proceedAndFinish(); - } else { - // If BT is not up yet, show "Turning off Bluetooth..." - mReceiver = new StateChangeReceiver(); - registerReceiver(mReceiver, new IntentFilter( - BluetoothAdapter.ACTION_STATE_CHANGED)); - createDialog(); - } - } break; - - default: { - cancelAndFinish(); - } break; + private void onDisableConfirmed() { + mBluetoothAdapter.disable(); + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { + proceedAndFinish(); + } else { + // If BT is not up yet, show "Turning off Bluetooth..." + mReceiver = new StateChangeReceiver(); + registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + createDialog(); } } diff --git a/src/com/android/settings/bluetooth/RequestPermissionHelper.kt b/src/com/android/settings/bluetooth/RequestPermissionHelper.kt new file mode 100644 index 00000000000..000a7d16295 --- /dev/null +++ b/src/com/android/settings/bluetooth/RequestPermissionHelper.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2023 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.settings.bluetooth + +import android.content.Context +import android.os.UserManager +import androidx.appcompat.app.AlertDialog +import com.android.settings.R +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager +import com.android.settingslib.spaprivileged.framework.common.userManager + +object RequestPermissionHelper { + fun requestEnable( + context: Context, + appLabel: CharSequence?, + timeout: Int, + onAllow: () -> Unit, + onDeny: () -> Unit, + ) { + if (context.resources.getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) { + // Don't even show the dialog if configured this way + onAllow() + return + } + AlertDialog.Builder(context).apply { + setMessage(context.getEnableMessage(timeout, appLabel)) + setPositiveButton(R.string.allow) { _, _ -> + if (context.isDisallowBluetooth()) onDeny() else onAllow() + } + setNegativeButton(R.string.deny) { _, _ -> onDeny() } + setOnCancelListener { onDeny() } + }.show() + } + + fun requestDisable( + context: Context, + appLabel: CharSequence?, + onAllow: () -> Unit, + onDeny: () -> Unit, + ) { + if (context.resources.getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) { + // Don't even show the dialog if configured this way + onAllow() + return + } + AlertDialog.Builder(context).apply { + setMessage(context.getDisableMessage(appLabel)) + setPositiveButton(R.string.allow) { _, _ -> onAllow() } + setNegativeButton(R.string.deny) { _, _ -> onDeny() } + setOnCancelListener { onDeny() } + }.show() + } +} + +// If Bluetooth is disallowed, don't try to enable it, show policy transparency message instead. +private fun Context.isDisallowBluetooth() = + if (userManager.hasUserRestriction(UserManager.DISALLOW_BLUETOOTH)) { + devicePolicyManager.createAdminSupportIntent(UserManager.DISALLOW_BLUETOOTH) + ?.let { startActivity(it) } + true + } else false + +private fun Context.getEnableMessage(timeout: Int, name: CharSequence?): String = when { + timeout < 0 -> when (name) { + null -> getString(R.string.bluetooth_ask_enablement_no_name) + else -> getString(R.string.bluetooth_ask_enablement, name) + } + + timeout == BluetoothDiscoverableEnabler.DISCOVERABLE_TIMEOUT_NEVER -> when (name) { + null -> getString(R.string.bluetooth_ask_enablement_and_lasting_discovery_no_name) + else -> getString(R.string.bluetooth_ask_enablement_and_lasting_discovery, name) + } + + else -> when (name) { + null -> getString(R.string.bluetooth_ask_enablement_and_discovery_no_name, timeout) + else -> getString(R.string.bluetooth_ask_enablement_and_discovery, name, timeout) + } +} + +private fun Context.getDisableMessage(name: CharSequence?): String = + when (name) { + null -> getString(R.string.bluetooth_ask_disablement_no_name) + else -> getString(R.string.bluetooth_ask_disablement, name) + } diff --git a/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java b/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java deleted file mode 100644 index 99ac9281510..00000000000 --- a/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2009 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.settings.bluetooth; - -import android.app.Activity; -import android.app.admin.DevicePolicyManager; -import android.bluetooth.BluetoothAdapter; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.os.UserManager; -import android.util.Log; - -import com.android.internal.app.AlertActivity; -import com.android.internal.app.AlertController; -import com.android.settings.R; - -/** - * RequestPermissionHelperActivity asks the user whether to toggle Bluetooth. - * - * TODO: This activity isn't needed - this should be folded in RequestPermissionActivity - */ -public class RequestPermissionHelperActivity extends AlertActivity implements - DialogInterface.OnClickListener { - private static final String TAG = "RequestPermissionHelperActivity"; - - public static final String ACTION_INTERNAL_REQUEST_BT_ON = - "com.android.settings.bluetooth.ACTION_INTERNAL_REQUEST_BT_ON"; - - public static final String ACTION_INTERNAL_REQUEST_BT_OFF = - "com.android.settings.bluetooth.ACTION_INTERNAL_REQUEST_BT_OFF"; - - public static final String EXTRA_APP_LABEL = - "com.android.settings.bluetooth.extra.APP_LABEL"; - - private BluetoothAdapter mBluetoothAdapter; - - private CharSequence mAppLabel; - - private int mTimeout = -1; - - private int mRequest; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setResult(RESULT_CANCELED); - - // Note: initializes mBluetoothAdapter and returns true on error - if (!parseIntent()) { - finish(); - return; - } - - if (getResources().getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) { - // Don't even show the dialog if configured this way - onClick(null, BUTTON_POSITIVE); - dismiss(); - } - - createDialog(); - } - - void createDialog() { - final AlertController.AlertParams p = mAlertParams; - - switch (mRequest) { - case RequestPermissionActivity.REQUEST_ENABLE: { - if (mTimeout < 0) { - p.mMessage = mAppLabel != null - ? getString(R.string.bluetooth_ask_enablement, mAppLabel) - : getString(R.string.bluetooth_ask_enablement_no_name); - } else if (mTimeout == BluetoothDiscoverableEnabler.DISCOVERABLE_TIMEOUT_NEVER) { - p.mMessage = mAppLabel != null - ? getString( - R.string.bluetooth_ask_enablement_and_lasting_discovery, - mAppLabel) - : getString( - R.string.bluetooth_ask_enablement_and_lasting_discovery_no_name); - } else { - p.mMessage = mAppLabel != null - ? getString(R.string.bluetooth_ask_enablement_and_discovery, - mAppLabel, mTimeout) - : getString(R.string.bluetooth_ask_enablement_and_discovery_no_name, - mTimeout); - } - } break; - - case RequestPermissionActivity.REQUEST_DISABLE: { - p.mMessage = mAppLabel != null - ? getString(R.string.bluetooth_ask_disablement, mAppLabel) - : getString(R.string.bluetooth_ask_disablement_no_name); - } break; - } - - p.mPositiveButtonText = getString(R.string.allow); - p.mPositiveButtonListener = this; - p.mNegativeButtonText = getString(R.string.deny); - - setupAlert(); - } - - public void onClick(DialogInterface dialog, int which) { - switch (mRequest) { - case RequestPermissionActivity.REQUEST_ENABLE: - case RequestPermissionActivity.REQUEST_ENABLE_DISCOVERABLE: { - 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); - } - } else { - mBluetoothAdapter.enable(); - setResult(Activity.RESULT_OK); - } - } break; - - case RequestPermissionActivity.REQUEST_DISABLE: { - mBluetoothAdapter.disable(); - setResult(Activity.RESULT_OK); - } break; - } - } - - /** - * Parse the received Intent and initialize mBluetoothAdapter. - * @return true if an error occurred; false otherwise - */ - private boolean parseIntent() { - Intent intent = getIntent(); - if (intent == null) { - return false; - } - - String action = intent.getAction(); - if (ACTION_INTERNAL_REQUEST_BT_ON.equals(action)) { - mRequest = RequestPermissionActivity.REQUEST_ENABLE; - if (intent.hasExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION)) { - // Value used for display purposes. Not range checking. - mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, - BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT); - } - } else if (ACTION_INTERNAL_REQUEST_BT_OFF.equals(action)) { - mRequest = RequestPermissionActivity.REQUEST_DISABLE; - } else { - return false; - } - - mAppLabel = getIntent().getCharSequenceExtra(EXTRA_APP_LABEL); - - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (mBluetoothAdapter == null) { - Log.e(TAG, "Error: there's a problem starting Bluetooth"); - return false; - } - - return true; - } -} diff --git a/tests/robotests/src/com/android/settings/bluetooth/RequestPermissionHelperTest.kt b/tests/robotests/src/com/android/settings/bluetooth/RequestPermissionHelperTest.kt new file mode 100644 index 00000000000..ce0792c468a --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/RequestPermissionHelperTest.kt @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2023 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.settings.bluetooth + +import androidx.activity.ComponentActivity +import com.android.settings.R +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.robolectric.RobolectricTestRunner +import org.robolectric.android.controller.ActivityController +import org.robolectric.annotation.Config +import org.mockito.Mockito.`when` as whenever + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowAlertDialogCompat::class]) +class RequestPermissionHelperTest { + private lateinit var activityController: ActivityController + + @Before + fun setUp() { + activityController = + ActivityController.of(ComponentActivity()).create().start().postCreate(null).resume() + } + + @After + fun tearDown() { + activityController.pause().stop().destroy() + } + + @Test + fun requestEnable_withAppLabelAndNoTimeout_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = "App Label", + timeout = -1, + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs("App Label wants to turn on Bluetooth") + } + + @Test + fun requestEnable_withAppLabelAndZeroTimeout_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = "App Label", + timeout = 0, + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs( + "App Label wants to turn on Bluetooth and make your phone visible to other devices. " + + "You can change this later in Bluetooth settings." + ) + } + + @Test + fun requestEnable_withAppLabelAndNormalTimeout_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = "App Label", + timeout = 120, + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs( + "App Label wants to turn on Bluetooth and make your phone visible to other devices " + + "for 120 seconds." + ) + } + + @Test + fun requestEnable_withNoAppLabelAndNoTimeout_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = null, + timeout = -1, + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs("An app wants to turn on Bluetooth") + } + + @Test + fun requestEnable_withNoAppLabelAndZeroTimeout_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = null, + timeout = 0, + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs( + "An app wants to turn on Bluetooth and make your phone visible to other devices. " + + "You can change this later in Bluetooth settings." + ) + } + + @Test + fun requestEnable_withNoAppLabelAndNormalTimeout_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = null, + timeout = 120, + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs( + "An app wants to turn on Bluetooth and make your phone visible to other devices for " + + "120 seconds." + ) + } + + @Test + fun requestEnable_whenAutoConfirm_onAllowIsCalled() { + val activity = getActivityWith(autoConfirm = true) + var onAllowCalled = false + + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = null, + timeout = -1, + onAllow = { onAllowCalled = true }, + onDeny = {}, + ) + + assertThat(onAllowCalled).isTrue() + } + + @Test + fun requestEnable_whenNotAutoConfirm_onAllowIsNotCalledWhenRequest() { + val activity = getActivityWith(autoConfirm = false) + var onAllowCalled = false + + RequestPermissionHelper.requestEnable( + context = activity, + appLabel = null, + timeout = -1, + onAllow = { onAllowCalled = true }, + onDeny = {}, + ) + + assertThat(onAllowCalled).isFalse() + } + + @Test + fun requestDisable_withAppLabel_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestDisable( + context = activity, + appLabel = "App Label", + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs("App Label wants to turn off Bluetooth") + } + + @Test + fun requestDisable_withNoAppLabel_hasCorrectMessage() { + val activity = activityController.get() + RequestPermissionHelper.requestDisable( + context = activity, + appLabel = null, + onAllow = {}, + onDeny = {}, + ) + + assertLatestMessageIs("An app wants to turn off Bluetooth") + } + + @Test + fun requestDisable_whenAutoConfirm_onAllowIsCalled() { + val activity = getActivityWith(autoConfirm = true) + var onAllowCalled = false + + RequestPermissionHelper.requestDisable( + context = activity, + appLabel = null, + onAllow = { onAllowCalled = true }, + onDeny = {}, + ) + + assertThat(onAllowCalled).isTrue() + } + + @Test + fun requestDisable_whenNotAutoConfirm_onAllowIsNotCalledWhenRequest() { + val activity = getActivityWith(autoConfirm = false) + var onAllowCalled = false + + RequestPermissionHelper.requestDisable( + context = activity, + appLabel = null, + onAllow = { onAllowCalled = true }, + onDeny = {}, + ) + + assertThat(onAllowCalled).isFalse() + } + + private fun getActivityWith(autoConfirm: Boolean): ComponentActivity { + val activity = spy(activityController.get()) + val spyResources = spy(activity.resources) + whenever(activity.resources).thenReturn(spyResources) + whenever(spyResources.getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) + .thenReturn(autoConfirm) + return activity + } + + private fun assertLatestMessageIs(message: String) { + val dialog = ShadowAlertDialogCompat.getLatestAlertDialog() + val shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog) + assertThat(shadowDialog.message.toString()).isEqualTo(message) + } +}