From 13088cac84ba0aeb03deeb87af1eacc79b7bdeb0 Mon Sep 17 00:00:00 2001 From: Svetoslav Ganov Date: Fri, 22 Jul 2016 20:08:41 -0700 Subject: [PATCH] Add Bluetooth toggle prompts - settings If permission review is enabled toggling bluetoth on or off results in a user prompt to collect consent. This applies only to legacy apps, i.e. ones that don't support runtime permissions as they target SDK 22. bug:28715749 Change-Id: I5ae0c532c92b2c05a91f0d769ca6744002747fca (cherry picked from commit b06766f129dac138568883110f6d2a085b53445b) --- AndroidManifest.xml | 3 +- res/values/strings.xml | 3 + res/values/themes.xml | 4 + .../bluetooth/RequestPermissionActivity.java | 284 +++++++++++------- .../RequestPermissionHelperActivity.java | 134 ++++----- 5 files changed, 239 insertions(+), 189 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 48e8c03bab9..f09f6774f1f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2025,10 +2025,11 @@ android:label="@string/bluetooth_permission_request" android:excludeFromRecents="true" android:permission="android.permission.BLUETOOTH" - android:theme="@*android:style/Theme.Material.Light.Dialog.Alert"> + android:theme="@style/BluetoothPermission"> + diff --git a/res/values/strings.xml b/res/values/strings.xml index 3a377055cac..bdb9fa27fe5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -263,6 +263,9 @@ An app wants to turn Bluetooth ON for this device. + + An app wants to turn Bluetooth OFF for this device. + An app wants to make your tablet visible to other Bluetooth devices for %1$d seconds. diff --git a/res/values/themes.xml b/res/values/themes.xml index ef9e1e08a5a..4e3c774d7bf 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -314,4 +314,8 @@ #00000000 + + diff --git a/src/com/android/settings/bluetooth/RequestPermissionActivity.java b/src/com/android/settings/bluetooth/RequestPermissionActivity.java index 3148aedca58..9cfcc6b689d 100644 --- a/src/com/android/settings/bluetooth/RequestPermissionActivity.java +++ b/src/com/android/settings/bluetooth/RequestPermissionActivity.java @@ -42,61 +42,32 @@ public class RequestPermissionActivity extends Activity implements // Command line to test this // adb shell am start -a android.bluetooth.adapter.action.REQUEST_ENABLE // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISCOVERABLE + // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISABLE private static final String TAG = "RequestPermissionActivity"; private static final int MAX_DISCOVERABLE_TIMEOUT = 3600; // 1 hr - // Non-error return code: BT is starting or has started successfully. Used - // by this Activity and RequestPermissionHelperActivity - /* package */ static final int RESULT_BT_STARTING_OR_STARTED = -1000; - - private static final int REQUEST_CODE_START_BT = 1; + static final int REQUEST_ENABLE = 1; + static final int REQUEST_ENABLE_DISCOVERABLE = 2; + static final int REQUEST_DISABLE = 3; private LocalBluetoothAdapter mLocalAdapter; private int mTimeout = BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT; - /* - * True if bluetooth wasn't enabled and RequestPermissionHelperActivity was - * started to ask the user and start bt. - * - * If/when that activity returns successfully, display please wait msg then - * go away when bt has started and discovery mode has been enabled. - */ - private boolean mNeededToEnableBluetooth; - - // True if requesting BT to be turned on - // False if requesting BT to be turned on + discoverable mode - private boolean mEnableOnly; - - private boolean mUserConfirmed; + private int mRequest; private AlertDialog mDialog; - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (intent == null) { - return; - } - if (mNeededToEnableBluetooth - && BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothDevice.ERROR); - if (state == BluetoothAdapter.STATE_ON) { - if (mUserConfirmed) { - proceedAndFinish(); - } - } - } - } - }; + private BroadcastReceiver mReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setResult(Activity.RESULT_CANCELED); + // Note: initializes mLocalAdapter and returns true on error if (parseIntent()) { finish(); @@ -105,68 +76,98 @@ public class RequestPermissionActivity extends Activity implements int btState = mLocalAdapter.getState(); - 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. By kicking this to the - * RequestPermissionHelperActivity, this class will handle that - * case via the broadcast receiver. - */ - - /* - * Start the helper activity to: - * 1) ask the user about enabling bt AND discovery - * 2) enable BT upon confirmation - */ - registerReceiver(mReceiver, - new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - Intent intent = new Intent(); - intent.setClass(this, RequestPermissionHelperActivity.class); - if (mEnableOnly) { - intent.setAction(RequestPermissionHelperActivity.ACTION_INTERNAL_REQUEST_BT_ON); - } else { - intent.setAction(RequestPermissionHelperActivity. - ACTION_INTERNAL_REQUEST_BT_ON_AND_DISCOVERABLE); - intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, mTimeout); - } - startActivityForResult(intent, REQUEST_CODE_START_BT); - mNeededToEnableBluetooth = true; - break; - case BluetoothAdapter.STATE_ON: - if (mEnableOnly) { - // Nothing to do. Already enabled. + if (mRequest == REQUEST_DISABLE) { + switch (btState) { + case BluetoothAdapter.STATE_OFF: + case BluetoothAdapter.STATE_TURNING_OFF: { proceedAndFinish(); - } else { - // Ask the user about enabling discovery mode - createDialog(); - } - break; - default: - Log.e(TAG, "Unknown adapter state: " + btState); + } break; + + case BluetoothAdapter.STATE_ON: + case BluetoothAdapter.STATE_TURNING_ON: { + Intent intent = new Intent(this, RequestPermissionHelperActivity.class); + intent.setAction(RequestPermissionHelperActivity + .ACTION_INTERNAL_REQUEST_BT_OFF); + startActivityForResult(intent, 0); + } break; + + default: { + Log.e(TAG, "Unknown adapter state: " + btState); + cancelAndFinish(); + } break; + } + } else { + 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. By kicking this to the + * RequestPermissionHelperActivity, this class will handle that + * 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); + if (mRequest == REQUEST_ENABLE_DISCOVERABLE) { + intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, mTimeout); + } + startActivityForResult(intent, 0); + } break; + + case BluetoothAdapter.STATE_ON: { + if (mRequest == REQUEST_ENABLE) { + // Nothing to do. Already enabled. + proceedAndFinish(); + } else { + // Ask the user about enabling discovery mode + createDialog(); + } + } break; + + default: { + Log.e(TAG, "Unknown adapter state: " + btState); + cancelAndFinish(); + } break; + } } } private void createDialog() { + if (getResources().getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) { + onClick(null, DialogInterface.BUTTON_POSITIVE); + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(this); - if (mNeededToEnableBluetooth) { - // RequestPermissionHelperActivity has gotten confirmation from user - // to turn on BT - builder.setMessage(getString(R.string.bluetooth_turning_on)); + // Non-null receiver means we are toggling + if (mReceiver != null) { + switch (mRequest) { + case REQUEST_ENABLE: + case REQUEST_ENABLE_DISCOVERABLE: { + builder.setMessage(getString(R.string.bluetooth_turning_on)); + } break; + + default: { + builder.setMessage(getString(R.string.bluetooth_turning_off)); + } break; + } builder.setCancelable(false); } else { // Ask the user whether to turn on discovery mode or not // For lasting discoverable mode there is a different message if (mTimeout == BluetoothDiscoverableEnabler.DISCOVERABLE_TIMEOUT_NEVER) { - builder.setMessage( - getString(R.string.bluetooth_ask_lasting_discovery)); + builder.setMessage(getString(R.string.bluetooth_ask_lasting_discovery)); } else { - builder.setMessage( - getString(R.string.bluetooth_ask_discovery, mTimeout)); + builder.setMessage(getString(R.string.bluetooth_ask_discovery, mTimeout)); } builder.setPositiveButton(getString(R.string.allow), this); builder.setNegativeButton(getString(R.string.deny), this); @@ -174,36 +175,44 @@ public class RequestPermissionActivity extends Activity implements mDialog = builder.create(); mDialog.show(); - - if (getResources().getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog) == true) { - // dismiss dialog immediately if settings say so - onClick(null, DialogInterface.BUTTON_POSITIVE); - } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode != REQUEST_CODE_START_BT) { - Log.e(TAG, "Unexpected onActivityResult " + requestCode + ' ' + resultCode); - setResult(RESULT_CANCELED); - finish(); - return; - } - if (resultCode != RESULT_BT_STARTING_OR_STARTED) { - setResult(resultCode); - finish(); + if (resultCode != Activity.RESULT_OK) { + cancelAndFinish(); return; } - // Back from RequestPermissionHelperActivity. User confirmed to enable - // BT and discoverable mode. - mUserConfirmed = true; + switch (mRequest) { + case REQUEST_ENABLE: + case REQUEST_ENABLE_DISCOVERABLE: { + if (mLocalAdapter.getBluetoothState() == 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; - if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON) { - proceedAndFinish(); - } else { - // If BT is not up yet, show "Turning on Bluetooth..." - createDialog(); + case REQUEST_DISABLE: { + if (mLocalAdapter.getBluetoothState() == 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; } } @@ -223,8 +232,8 @@ public class RequestPermissionActivity extends Activity implements private void proceedAndFinish() { int returnCode; - if (mEnableOnly) { - // BT enabled. Done + if (mRequest == REQUEST_ENABLE || mRequest == REQUEST_DISABLE) { + // BT toggled. Done returnCode = RESULT_OK; } else if (mLocalAdapter.setScanMode( BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, mTimeout)) { @@ -252,16 +261,26 @@ public class RequestPermissionActivity extends Activity implements finish(); } + private void cancelAndFinish() { + setResult(Activity.RESULT_CANCELED); + finish(); + } + /** * Parse the received Intent and initialize mLocalBluetoothAdapter. * @return true if an error occurred; false otherwise */ private boolean parseIntent() { Intent intent = getIntent(); - if (intent != null && intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_ENABLE)) { - mEnableOnly = true; - } else if (intent != null - && intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)) { + if (intent == null) { + return true; + } + if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_ENABLE)) { + mRequest = REQUEST_ENABLE; + } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISABLE)) { + mRequest = REQUEST_DISABLE; + } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)) { + mRequest = REQUEST_ENABLE_DISCOVERABLE; mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT); @@ -292,8 +311,9 @@ public class RequestPermissionActivity extends Activity implements @Override protected void onDestroy() { super.onDestroy(); - if (mNeededToEnableBluetooth) { + if (mReceiver != null) { unregisterReceiver(mReceiver); + mReceiver = null; } } @@ -302,4 +322,38 @@ public class RequestPermissionActivity extends Activity implements setResult(RESULT_CANCELED); super.onBackPressed(); } + + private final class StateChangeReceiver extends BroadcastReceiver { + private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec + + public StateChangeReceiver() { + getWindow().getDecorView().postDelayed(() -> { + if (!isFinishing() && !isDestroyed()) { + cancelAndFinish(); + } + }, TOGGLE_TIMEOUT_MILLIS); + } + + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + final int currentState = intent.getIntExtra( + BluetoothAdapter.EXTRA_STATE, BluetoothDevice.ERROR); + switch (mRequest) { + case REQUEST_ENABLE: + case REQUEST_ENABLE_DISCOVERABLE: { + if (currentState == BluetoothAdapter.STATE_ON) { + proceedAndFinish(); + } + } break; + + case REQUEST_DISABLE: { + if (currentState == BluetoothAdapter.STATE_OFF) { + proceedAndFinish(); + } + } break; + } + } + } } diff --git a/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java b/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java index fafa2842f15..d70a63f9c50 100644 --- a/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java +++ b/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java @@ -16,6 +16,7 @@ package com.android.settings.bluetooth; +import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.content.DialogInterface; import android.content.Intent; @@ -24,106 +25,94 @@ import android.util.Log; import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; +import com.android.internal.util.ArrayUtils; import com.android.settings.R; import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; /** - * RequestPermissionHelperActivity asks the user whether to enable discovery. - * This is usually started by RequestPermissionActivity. + * 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"; + "com.android.settings.bluetooth.ACTION_INTERNAL_REQUEST_BT_ON"; - public static final String ACTION_INTERNAL_REQUEST_BT_ON_AND_DISCOVERABLE = - "com.android.settings.bluetooth.ACTION_INTERNAL_REQUEST_BT_ON_AND_DISCOVERABLE"; + public static final String ACTION_INTERNAL_REQUEST_BT_OFF = + "com.android.settings.bluetooth.ACTION_INTERNAL_REQUEST_BT_OFF"; private LocalBluetoothAdapter mLocalAdapter; - private int mTimeout; + private int mTimeout = -1; - // True if requesting BT to be turned on - // False if requesting BT to be turned on + discoverable mode - private boolean mEnableOnly; + private int mRequest; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setResult(RESULT_CANCELED); + // Note: initializes mLocalAdapter and returns true on error - if (parseIntent()) { + if (!parseIntent()) { finish(); return; } - createDialog(); - - if (getResources().getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog) == true) { - // dismiss dialog immediately if settings say so + 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; - if (mEnableOnly) { - p.mMessage = getString(R.string.bluetooth_ask_enablement); - } else { - if (mTimeout == BluetoothDiscoverableEnabler.DISCOVERABLE_TIMEOUT_NEVER) { - p.mMessage = getString(R.string.bluetooth_ask_enablement_and_lasting_discovery); - } else { - p.mMessage = getString(R.string.bluetooth_ask_enablement_and_discovery, mTimeout); - } + switch (mRequest) { + case RequestPermissionActivity.REQUEST_ENABLE: { + if (mTimeout < 0) { + p.mMessage = getString(R.string.bluetooth_ask_enablement); + } else if (mTimeout == BluetoothDiscoverableEnabler.DISCOVERABLE_TIMEOUT_NEVER) { + p.mMessage = getString( + R.string.bluetooth_ask_enablement_and_lasting_discovery); + } else { + p.mMessage = getString( + R.string.bluetooth_ask_enablement_and_discovery, mTimeout); + } + } break; + + case RequestPermissionActivity.REQUEST_DISABLE: { + p.mMessage = getString(R.string.bluetooth_ask_disablement); + } break; } p.mPositiveButtonText = getString(R.string.allow); p.mPositiveButtonListener = this; p.mNegativeButtonText = getString(R.string.deny); - p.mNegativeButtonListener = this; setupAlert(); } public void onClick(DialogInterface dialog, int which) { - int returnCode; - // FIXME: fix this ugly switch logic! - switch (which) { - case BUTTON_POSITIVE: - int btState = 0; + switch (mRequest) { + case RequestPermissionActivity.REQUEST_ENABLE: + case RequestPermissionActivity.REQUEST_ENABLE_DISCOVERABLE: { + mLocalAdapter.enable(); + setResult(Activity.RESULT_OK); + } break; - try { - // TODO There's a better way. - int retryCount = 30; - do { - btState = mLocalAdapter.getBluetoothState(); - Thread.sleep(100); - } while (btState == BluetoothAdapter.STATE_TURNING_OFF && --retryCount > 0); - } catch (InterruptedException ignored) { - // don't care - } - - if (btState == BluetoothAdapter.STATE_TURNING_ON - || btState == BluetoothAdapter.STATE_ON - || mLocalAdapter.enable()) { - returnCode = RequestPermissionActivity.RESULT_BT_STARTING_OR_STARTED; - } else { - returnCode = RESULT_CANCELED; - } - break; - - case BUTTON_NEGATIVE: - returnCode = RESULT_CANCELED; - break; - default: - return; + case RequestPermissionActivity.REQUEST_DISABLE: { + mLocalAdapter.disable(); + setResult(Activity.RESULT_OK); + } break; } - setResult(returnCode); } /** @@ -132,33 +121,32 @@ public class RequestPermissionHelperActivity extends AlertActivity implements */ private boolean parseIntent() { Intent intent = getIntent(); - if (intent != null && intent.getAction().equals(ACTION_INTERNAL_REQUEST_BT_ON)) { - mEnableOnly = true; - } else if (intent != null - && intent.getAction().equals(ACTION_INTERNAL_REQUEST_BT_ON_AND_DISCOVERABLE)) { - mEnableOnly = false; - // Value used for display purposes. Not range checking. - mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, - BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT); + 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 { - setResult(RESULT_CANCELED); - return true; + return false; } LocalBluetoothManager manager = Utils.getLocalBtManager(this); if (manager == null) { Log.e(TAG, "Error: there's a problem starting Bluetooth"); - setResult(RESULT_CANCELED); - return true; + return false; } + mLocalAdapter = manager.getBluetoothAdapter(); - return false; - } - - @Override - public void onBackPressed() { - setResult(RESULT_CANCELED); - super.onBackPressed(); + return true; } }