Merge "Action intents for bluetooth" into pi-car-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 47c8256..a94f42c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -263,8 +263,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 cda2da0..244f3bb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -305,6 +305,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 731ee0b..ec3c88b 100644
--- a/src/com/android/car/settings/bluetooth/BluetoothUtils.java
+++ b/src/com/android/car/settings/bluetooth/BluetoothUtils.java
@@ -168,6 +168,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;
+    }
+}