Merge "Action intents for bluetooth" into pi-car-dev
am: ad11bf518e
Change-Id: Ie684c0d0ef7687e1b0a045c3e45e46c870088572
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d84803d..7666f61 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -256,8 +256,12 @@
</activity>
<activity android:name=".bluetooth.BluetoothRequestPermissionActivity"
- android:excludeFromRecents="true">
+ android:theme="@style/ActionDialogTheme"
+ android:excludeFromRecents="true"
+ android:clearTaskOnLaunch="true"
+ android:launchMode="singleInstance">
<intent-filter>
+ <action android:name="android.bluetooth.adapter.action.REQUEST_DISCOVERABLE" />
<action android:name="android.bluetooth.adapter.action.REQUEST_ENABLE" />
<action android:name="android.bluetooth.adapter.action.REQUEST_DISABLE" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0d9a62c..77800ae 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -352,6 +352,14 @@
<string name="bluetooth_ask_enablement_no_name">An app wants to turn on Bluetooth</string>
<!-- This string asks the user whether or not to allow an app to disable bluetooth. [CHAR LIMIT=100] -->
<string name="bluetooth_ask_disablement_no_name">An app wants to turn off Bluetooth</string>
+ <!-- Strings for asking to the user whether to allow an app to enable discovery mode. [CHAR LIMIT=NONE] -->
+ <string name="bluetooth_ask_discovery"><xliff:g id="app_name">%1$s</xliff:g> wants to make your headunit visible to other Bluetooth devices for <xliff:g id="timeout">%2$d</xliff:g> seconds.</string>
+ <!-- Strings for asking to the user whether to allow an app to enable discovery mode. [CHAR LIMIT=NONE] -->
+ <string name="bluetooth_ask_discovery_no_name">An app wants to make your headunit visible to other Bluetooth devices for <xliff:g id="timeout">%1$d</xliff:g> seconds.</string>
+ <!-- Strings for asking to the user whether to allow an app to enable bluetooth and discovery mode. [CHAR LIMIT=NONE] -->
+ <string name="bluetooth_ask_enablement_and_discovery"><xliff:g id="app_name">%1$s</xliff:g> wants to turn on Bluetooth and make your headunit visible to other devices for <xliff:g id="timeout">%2$d</xliff:g> seconds.</string>
+ <!-- Strings for asking to the user whether to allow an app to enable bluetooth and discovery mode. [CHAR LIMIT=NONE] -->
+ <string name="bluetooth_ask_enablement_and_discovery_no_name">An app wants to turn on Bluetooth and make your headunit visible to other devices for <xliff:g id="timeout">%1$d</xliff:g> seconds.</string>
<!-- Bluetooth pairing --><skip/>
<!-- Notification ticker text (shown in the status bar) when a Bluetooth device wants to pair with us -->
diff --git a/res/values/themes.xml b/res/values/themes.xml
index ac64921..d2dc80e 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -65,6 +65,14 @@
<item name="android:navigationBarColor">#00000000</item>
</style>
+ <style name="ActionDialogTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation</item>
+ </style>
+
<!-- Themes for Setup Wizard -->
<style name="FallbackHome.SetupWizard"
diff --git a/src/com/android/car/settings/bluetooth/BluetoothRequestPermissionActivity.java b/src/com/android/car/settings/bluetooth/BluetoothRequestPermissionActivity.java
index c285c3d..41a5a79 100644
--- a/src/com/android/car/settings/bluetooth/BluetoothRequestPermissionActivity.java
+++ b/src/com/android/car/settings/bluetooth/BluetoothRequestPermissionActivity.java
@@ -32,47 +32,72 @@
import android.os.Bundle;
import android.os.UserManager;
import android.text.TextUtils;
+import android.util.Log;
+
+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;
/**
- * Code drop from {@link com.android.settings.bluetooth.RequestPermissionActivity}.
- *
* This {@link Activity} handles requests to toggle Bluetooth by collecting user
- * consent and waiting until the state change is completed.
+ * 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 implements
- DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+public class BluetoothRequestPermissionActivity extends Activity {
private static final Logger LOG = new Logger(BluetoothRequestPermissionActivity.class);
- private static final int REQUEST_UNKNOWN = 0;
- private static final int REQUEST_ENABLE = 1;
- private static final int REQUEST_DISABLE = 2;
+
+ @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 AlertDialog mDialog;
private StateChangeReceiver mReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setResult(Activity.RESULT_CANCELED);
-
mRequest = parseIntent();
if (mRequest == REQUEST_UNKNOWN) {
- finish();
+ finishWithResult(RESULT_CANCELED);
return;
}
- mReceiver = new StateChangeReceiver();
+ 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();
+ Log.e("TEST_TEST", "request: " + mRequest + " state: " + btState);
switch (mRequest) {
case REQUEST_DISABLE:
switch (btState) {
@@ -83,12 +108,13 @@
case BluetoothAdapter.STATE_ON:
case BluetoothAdapter.STATE_TURNING_ON:
- createDecisionDialog();
+ mDialog = createRequestDisableBluetoothDialog();
+ mDialog.show();
break;
default:
LOG.e("Unknown adapter state: " + btState);
- finish();
+ finishWithResult(RESULT_CANCELED);
break;
}
break;
@@ -96,163 +122,90 @@
switch (btState) {
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
- createDecisionDialog();
+ mDialog = createRequestEnableBluetoothDialog();
+ mDialog.show();
break;
-
case BluetoothAdapter.STATE_ON:
+ case BluetoothAdapter.STATE_TURNING_ON:
proceedAndFinish();
break;
default:
LOG.e("Unknown adapter state: " + btState);
- finish();
+ finishWithResult(RESULT_CANCELED);
break;
}
- }
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- mReceiver.register();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mReceiver.unregister();
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- switch (which) {
- case DialogInterface.BUTTON_POSITIVE:
- proceedAndFinish();
break;
-
- case DialogInterface.BUTTON_NEGATIVE:
- onCancel(/* dialog = */ null);
- break;
- }
- }
-
- @Override
- public void onCancel(DialogInterface dialog) {
- setResult(Activity.RESULT_CANCELED);
- finish();
- }
-
- private void createInterimDialog() {
- AlertDialog.Builder builder = new AlertDialog.Builder(/* context = */ this);
-
- switch (mRequest) {
- case REQUEST_ENABLE:
- builder.setMessage(getString(R.string.bluetooth_turning_on));
- break;
- default:
- builder.setMessage(getString(R.string.bluetooth_turning_off));
- break;
- }
- builder.setCancelable(false).setOnCancelListener(/* listener = */ this);
-
- mDialog = builder.create();
- mDialog.show();
- }
-
- private void createDecisionDialog() {
- AlertDialog.Builder builder = new AlertDialog.Builder(/* context= */ this);
- switch (mRequest) {
- case REQUEST_ENABLE:
- builder.setMessage(
- mAppLabel != null ? getString(R.string.bluetooth_ask_enablement,
- mAppLabel)
- : getString(R.string.bluetooth_ask_enablement_no_name));
- break;
-
- case REQUEST_DISABLE: {
- builder.setMessage(
- mAppLabel != null ? getString(R.string.bluetooth_ask_disablement, mAppLabel)
- : getString(R.string.bluetooth_ask_disablement_no_name));
- break;
- }
- }
-
- builder.setPositiveButton(R.string.allow, this::decisionDialogPositiveButtonListener)
- .setNegativeButton(R.string.deny, (dialog, which) -> onCancel(/* dialog = */ null))
- .setOnCancelListener(/* listener = */ this);
-
- mDialog = builder.create();
- mDialog.show();
- }
-
- private void decisionDialogPositiveButtonListener(DialogInterface dialog, int which) {
- dialog.dismiss();
-
- if (!hasUserRestriction()) {
- switch (mRequest) {
- case REQUEST_ENABLE:
- if (mLocalBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
- proceedAndFinish();
- } else {
- // If BT is not up yet, show "Turning on Bluetooth..."
- createInterimDialog();
- }
- break;
-
- case REQUEST_DISABLE:
- if (mLocalBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
- proceedAndFinish();
- } else {
- // If BT is not up yet, show "Turning off Bluetooth..."
- createInterimDialog();
- }
- break;
-
- default:
- finish();
- break;
- }
- }
- }
-
- private boolean hasUserRestriction() {
- switch (mRequest) {
- case REQUEST_ENABLE:
- 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 true;
- } else {
- mLocalBluetoothAdapter.enable();
+ 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;
-
- case REQUEST_DISABLE: {
- mLocalBluetoothAdapter.disable();
- }
- break;
}
+ }
- return false;
+ @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 = REQUEST_UNKNOWN;
+ int request;
Intent intent = getIntent();
if (intent == null) {
return REQUEST_UNKNOWN;
@@ -265,6 +218,15 @@
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);
@@ -273,7 +235,7 @@
String packageName = getCallingPackage();
if (TextUtils.isEmpty(packageName)) {
- packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+ packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
}
if (!TextUtils.isEmpty(packageName)) {
try {
@@ -286,63 +248,175 @@
}
}
- mLocalBluetoothManager = LocalBluetoothManager.getInstance(
- getApplicationContext(), /* onInitCallback= */ null);
- if (mLocalBluetoothManager == null) {
- LOG.e("Bluetooth is not supported on this device");
- return REQUEST_UNKNOWN;
- }
-
- mLocalBluetoothAdapter = mLocalBluetoothManager.getBluetoothAdapter();
- if (mLocalBluetoothAdapter == null) {
- LOG.e("Error: there's a problem starting Bluetooth");
- 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() {
+ StateChangeReceiver(int desiredState) {
+ mDesiredState = desiredState;
+
getWindow().getDecorView().postDelayed(() -> {
if (!isFinishing() && !isDestroyed()) {
- onCancel(null);
+ finishWithResult(RESULT_CANCELED);
}
}, TOGGLE_TIMEOUT_MILLIS);
}
- public void register() {
- registerReceiver(/* receiver= */ this,
- new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
- }
-
- public void unregister() {
- unregisterReceiver(/* receiver= */ this);
- }
-
+ @Override
public void onReceive(Context context, Intent intent) {
- Activity activity = BluetoothRequestPermissionActivity.this;
if (intent == null) {
return;
}
+
int currentState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothDevice.ERROR);
- switch (mRequest) {
- case REQUEST_ENABLE:
- if (currentState == BluetoothAdapter.STATE_ON) {
- activity.setResult(Activity.RESULT_OK);
- proceedAndFinish();
- }
- break;
-
- case REQUEST_DISABLE:
- if (currentState == BluetoothAdapter.STATE_OFF) {
- activity.setResult(Activity.RESULT_OK);
- proceedAndFinish();
- }
- break;
+ if (mDesiredState == currentState) {
+ proceedAndFinish();
}
}
}
diff --git a/src/com/android/car/settings/bluetooth/BluetoothUtils.java b/src/com/android/car/settings/bluetooth/BluetoothUtils.java
index b7f9fb9..d20e849 100644
--- a/src/com/android/car/settings/bluetooth/BluetoothUtils.java
+++ b/src/com/android/car/settings/bluetooth/BluetoothUtils.java
@@ -169,6 +169,12 @@
editor.apply();
}
+ static void persistDiscoverableEndTimestamp(Context context, long endTimestamp) {
+ SharedPreferences.Editor editor = getSharedPreferences(context).edit();
+ editor.putLong(KEY_DISCOVERABLE_END_TIMESTAMP, endTimestamp);
+ editor.apply();
+ }
+
public static LocalBluetoothManager getLocalBtManager(Context context) {
return LocalBluetoothManager.getInstance(context, mOnInitCallback);
}
diff --git a/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothRequestPermissionActivityTest.java b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothRequestPermissionActivityTest.java
new file mode 100644
index 0000000..28477b3
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothRequestPermissionActivityTest.java
@@ -0,0 +1,241 @@
+/*
+ * 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 static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.widget.Button;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.testutils.ShadowBluetoothAdapter;
+import com.android.car.settings.testutils.ShadowBluetoothPan;
+import com.android.car.settings.testutils.ShadowLocalBluetoothAdapter;
+import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowLocalBluetoothAdapter.class, ShadowBluetoothAdapter.class,
+ ShadowBluetoothPan.class})
+public class BluetoothRequestPermissionActivityTest {
+
+ private Context mContext;
+ private ActivityController<BluetoothRequestPermissionActivity> mActivityController;
+ private BluetoothRequestPermissionActivity mActivity;
+ private LocalBluetoothAdapter mAdapter;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = RuntimeEnvironment.application;
+ mActivityController = ActivityController.of(new BluetoothRequestPermissionActivity());
+ mActivity = mActivityController.get();
+
+ mAdapter = LocalBluetoothManager.getInstance(mContext,
+ /* onInitCallback= */ null).getBluetoothAdapter();
+
+ // Make sure controller is available.
+ Shadows.shadowOf(mContext.getPackageManager()).setSystemFeature(
+ FEATURE_BLUETOOTH, /* supported= */ true);
+ BluetoothAdapter.getDefaultAdapter().enable();
+ }
+
+ @Test
+ public void onCreate_requestDisableIntent_hasDisableRequestType() {
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getRequestType()).isEqualTo(
+ BluetoothRequestPermissionActivity.REQUEST_DISABLE);
+ }
+
+ @Test
+ public void onCreate_requestDiscoverableIntent_hasDiscoverableRequestType() {
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getRequestType()).isEqualTo(
+ BluetoothRequestPermissionActivity.REQUEST_ENABLE_DISCOVERABLE);
+ }
+
+ @Test
+ public void onCreate_requestDiscoverableIntent_noTimeoutSpecified_hasDefaultTimeout() {
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getTimeout()).isEqualTo(
+ BluetoothRequestPermissionActivity.DEFAULT_DISCOVERABLE_TIMEOUT);
+ }
+
+ @Test
+ public void onCreate_requestDiscoverableIntent_timeoutSpecified_hasTimeout() {
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,
+ BluetoothRequestPermissionActivity.MAX_DISCOVERABLE_TIMEOUT);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getTimeout()).isEqualTo(
+ BluetoothRequestPermissionActivity.MAX_DISCOVERABLE_TIMEOUT);
+ }
+
+ @Test
+ public void onCreate_requestEnableIntent_hasEnableRequestType() {
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getRequestType()).isEqualTo(
+ BluetoothRequestPermissionActivity.REQUEST_ENABLE);
+ }
+
+ @Test
+ public void onCreate_bluetoothOff_requestDisableIntent_noDialog() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_OFF);
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getCurrentDialog()).isNull();
+ }
+
+ @Test
+ public void onCreate_bluetoothOn_requestDisableIntent_startsDialog() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getCurrentDialog()).isNotNull();
+ assertThat(mActivity.getCurrentDialog().isShowing()).isTrue();
+ }
+
+ @Test
+ public void onCreate_bluetoothOff_requestDiscoverableIntent_startsDialog() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_OFF);
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getCurrentDialog()).isNotNull();
+ assertThat(mActivity.getCurrentDialog().isShowing()).isTrue();
+ }
+
+ @Test
+ public void onCreate_bluetoothOn_requestDiscoverableIntent_startsDialog() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getCurrentDialog()).isNotNull();
+ assertThat(mActivity.getCurrentDialog().isShowing()).isTrue();
+ }
+
+ @Test
+ public void onCreate_bluetoothOff_requestEnableIntent_startsDialog() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_OFF);
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getCurrentDialog()).isNotNull();
+ assertThat(mActivity.getCurrentDialog().isShowing()).isTrue();
+ }
+
+ @Test
+ public void onCreate_bluetoothOn_requestEnableIntent_noDialog() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ assertThat(mActivity.getCurrentDialog()).isNull();
+ }
+
+ @Test
+ public void onPositiveClick_disableDialog_disables() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
+ mAdapter.enable();
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ Button button = mActivity.getCurrentDialog().getButton(DialogInterface.BUTTON_POSITIVE);
+ button.performClick();
+
+ assertThat(mAdapter.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void onPositiveClick_discoverableDialog_scanModeSet() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
+ mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_NONE);
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ Button button = mActivity.getCurrentDialog().getButton(DialogInterface.BUTTON_POSITIVE);
+ button.performClick();
+
+ assertThat(mAdapter.getScanMode()).isEqualTo(
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+ }
+
+ @Test
+ public void onPositiveClick_enableDialog_enables() {
+ getShadowLocalBluetoothAdapter().setState(BluetoothAdapter.STATE_OFF);
+ mAdapter.disable();
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ mActivity.setIntent(intent);
+ mActivityController.create();
+
+ Button button = mActivity.getCurrentDialog().getButton(DialogInterface.BUTTON_POSITIVE);
+ button.performClick();
+
+ assertThat(mAdapter.isEnabled()).isTrue();
+ }
+
+ private ShadowLocalBluetoothAdapter getShadowLocalBluetoothAdapter() {
+ return (ShadowLocalBluetoothAdapter) Shadow.extract(mAdapter);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowLocalBluetoothAdapter.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowLocalBluetoothAdapter.java
new file mode 100644
index 0000000..da4d43b
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowLocalBluetoothAdapter.java
@@ -0,0 +1,74 @@
+/*
+ * 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.testutils;
+
+import android.bluetooth.BluetoothAdapter;
+
+import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(LocalBluetoothAdapter.class)
+public class ShadowLocalBluetoothAdapter {
+
+ private int mState = BluetoothAdapter.STATE_OFF;
+ private boolean mIsBluetoothEnabled = true;
+ private int mScanMode = BluetoothAdapter.SCAN_MODE_NONE;
+
+ @Implementation
+ protected boolean isEnabled() {
+ return mIsBluetoothEnabled;
+ }
+
+ @Implementation
+ protected boolean enable() {
+ mIsBluetoothEnabled = true;
+ return true;
+ }
+
+ @Implementation
+ protected boolean disable() {
+ mIsBluetoothEnabled = false;
+ return true;
+ }
+
+ @Implementation
+ protected int getScanMode() {
+ return mScanMode;
+ }
+
+ @Implementation
+ protected void setScanMode(int mode) {
+ mScanMode = mode;
+ }
+
+ @Implementation
+ protected boolean setScanMode(int mode, int duration) {
+ mScanMode = mode;
+ return true;
+ }
+
+ @Implementation
+ protected int getState() {
+ return mState;
+ }
+
+ public void setState(int state) {
+ mState = state;
+ }
+}