Merge "DO NOT MERGE Extract Bluetooth device scanning logic into abstract superclass." into pi-car-dev
diff --git a/src/com/android/car/settings/bluetooth/BluetoothScanningDevicesGroupPreferenceController.java b/src/com/android/car/settings/bluetooth/BluetoothScanningDevicesGroupPreferenceController.java
new file mode 100644
index 0000000..f39598a
--- /dev/null
+++ b/src/com/android/car/settings/bluetooth/BluetoothScanningDevicesGroupPreferenceController.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 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.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+/**
+ * Controller which sets the Bluetooth adapter to discovery mode and begins scanning for
+ * discoverable devices for as long as the preference group is shown. Discovery
+ * and scanning are halted while any device is pairing. Users with the {@link
+ * DISALLOW_CONFIG_BLUETOOTH} restriction cannot scan for devices, so only cached devices will be
+ * shown.
+ */
+public abstract class BluetoothScanningDevicesGroupPreferenceController extends
+        BluetoothDevicesGroupPreferenceController {
+
+    private static final Logger LOG = new Logger(
+            BluetoothScanningDevicesGroupPreferenceController.class);
+
+    private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    private final AlwaysDiscoverable mAlwaysDiscoverable;
+    private boolean mIsScanningEnabled;
+
+    public BluetoothScanningDevicesGroupPreferenceController(Context context, String preferenceKey,
+            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+        super(context, preferenceKey, fragmentController, uxRestrictions);
+        mAlwaysDiscoverable = new AlwaysDiscoverable(context, mBluetoothAdapter);
+    }
+
+    @Override
+    protected final void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
+        LOG.d("onDeviceClicked: " + cachedDevice);
+        disableScanning();
+        onDeviceClickedInternal(cachedDevice);
+    }
+
+    /**
+     * Called when the user selects a device in the group.
+     *
+     * @param cachedDevice the device represented by the selected preference.
+     */
+    protected abstract void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice);
+
+    @Override
+    protected void onStopInternal() {
+        super.onStopInternal();
+        disableScanning();
+        getBluetoothManager().getCachedDeviceManager().clearNonBondedDevices();
+        getPreferenceMap().clear();
+        getPreference().removeAll();
+    }
+
+    @Override
+    protected void updateState(PreferenceGroup preferenceGroup) {
+        super.updateState(preferenceGroup);
+        if (shouldEnableScanning()) {
+            enableScanning();
+        } else {
+            disableScanning();
+        }
+    }
+
+    private boolean shouldEnableScanning() {
+        for (CachedBluetoothDevice device : getPreferenceMap().keySet()) {
+            if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
+                return false;
+            }
+        }
+        // Users who cannot configure Bluetooth cannot scan.
+        return !getCarUserManagerHelper().isCurrentProcessUserHasRestriction(
+                DISALLOW_CONFIG_BLUETOOTH);
+    }
+
+    /**
+     * Starts scanning for devices which will be displayed in the group for a user to select.
+     * Calls are idempotent.
+     */
+    private void enableScanning() {
+        mIsScanningEnabled = true;
+        if (!mBluetoothAdapter.isDiscovering()) {
+            mBluetoothAdapter.startDiscovery();
+        }
+        mAlwaysDiscoverable.start();
+        getPreference().setEnabled(true);
+    }
+
+    /** Stops scanning for devices and disables interaction. Calls are idempotent. */
+    private void disableScanning() {
+        mIsScanningEnabled = false;
+        getPreference().setEnabled(false);
+        mAlwaysDiscoverable.stop();
+        if (mBluetoothAdapter.isDiscovering()) {
+            mBluetoothAdapter.cancelDiscovery();
+        }
+    }
+
+    @Override
+    public void onScanningStateChanged(boolean started) {
+        LOG.d("onScanningStateChanged started: " + started + " mIsScanningEnabled: "
+                + mIsScanningEnabled);
+        if (!started && mIsScanningEnabled) {
+            enableScanning();
+        }
+    }
+
+    @Override
+    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
+        LOG.d("onDeviceBondStateChanged device: " + cachedDevice + " state: " + bondState);
+        refreshUi();
+    }
+
+    /**
+     * Helper class to keep the {@link BluetoothAdapter} in discoverable mode indefinitely. By
+     * default, setting the scan mode to BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE will
+     * timeout, but for pairing, we want to keep the device discoverable as long as the page is
+     * scanning.
+     */
+    private static final class AlwaysDiscoverable extends BroadcastReceiver {
+
+        private final Context mContext;
+        private final BluetoothAdapter mAdapter;
+        private final IntentFilter mIntentFilter = new IntentFilter(
+                BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+
+        private boolean mStarted;
+
+        AlwaysDiscoverable(Context context, BluetoothAdapter adapter) {
+            mContext = context;
+            mAdapter = adapter;
+        }
+
+        /**
+         * Sets the adapter scan mode to
+         * {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE_DISCOVERABLE}. {@link #start()} calls
+         * should have a matching calls to {@link #stop()} when discover mode is no longer needed.
+         */
+        void start() {
+            if (mStarted) {
+                return;
+            }
+            mContext.registerReceiver(this, mIntentFilter);
+            mStarted = true;
+            setDiscoverable();
+        }
+
+        void stop() {
+            if (!mStarted) {
+                return;
+            }
+            mContext.unregisterReceiver(this);
+            mStarted = false;
+            mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            setDiscoverable();
+        }
+
+        private void setDiscoverable() {
+            if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+                mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+            }
+        }
+    }
+}
diff --git a/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceController.java b/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceController.java
index c98a274..b460baa 100644
--- a/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceController.java
+++ b/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceController.java
@@ -18,15 +18,8 @@
 
 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
 
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
 import android.car.drivingstate.CarUxRestrictions;
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-
-import androidx.preference.PreferenceGroup;
 
 import com.android.car.settings.R;
 import com.android.car.settings.common.FragmentController;
@@ -42,19 +35,14 @@
  * DISALLOW_CONFIG_BLUETOOTH} restriction cannot pair devices.
  */
 public class BluetoothUnbondedDevicesPreferenceController extends
-        BluetoothDevicesGroupPreferenceController {
+        BluetoothScanningDevicesGroupPreferenceController {
 
     private static final Logger LOG = new Logger(
             BluetoothUnbondedDevicesPreferenceController.class);
 
-    private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-    private final AlwaysDiscoverable mAlwaysDiscoverable;
-    private boolean mIsScanningEnabled;
-
     public BluetoothUnbondedDevicesPreferenceController(Context context, String preferenceKey,
             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
         super(context, preferenceKey, fragmentController, uxRestrictions);
-        mAlwaysDiscoverable = new AlwaysDiscoverable(context, mBluetoothAdapter);
     }
 
     @Override
@@ -63,9 +51,7 @@
     }
 
     @Override
-    protected void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
-        LOG.d("onDeviceClicked: " + cachedDevice);
-        disableScanning();
+    protected void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice) {
         if (cachedDevice.startPairing()) {
             LOG.d("startPairing");
             // Indicate that this client (vehicle) would like access to contacts (PBAP) and messages
@@ -89,125 +75,4 @@
         }
         return availabilityStatus;
     }
-
-    @Override
-    protected void onStopInternal() {
-        super.onStopInternal();
-        disableScanning();
-        getBluetoothManager().getCachedDeviceManager().clearNonBondedDevices();
-        getPreferenceMap().clear();
-        getPreference().removeAll();
-    }
-
-    @Override
-    protected void updateState(PreferenceGroup preferenceGroup) {
-        super.updateState(preferenceGroup);
-        if (shouldEnableScanning()) {
-            enableScanning();
-        } else {
-            disableScanning();
-        }
-    }
-
-    private boolean shouldEnableScanning() {
-        for (CachedBluetoothDevice device : getPreferenceMap().keySet()) {
-            if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Starts scanning for devices which will be displayed in the group for a user to select.
-     * Calls are idempotent.
-     */
-    private void enableScanning() {
-        mIsScanningEnabled = true;
-        if (!mBluetoothAdapter.isDiscovering()) {
-            mBluetoothAdapter.startDiscovery();
-        }
-        mAlwaysDiscoverable.start();
-        getPreference().setEnabled(true);
-    }
-
-    /** Stops scanning for devices and disables interaction. Calls are idempotent. */
-    private void disableScanning() {
-        mIsScanningEnabled = false;
-        getPreference().setEnabled(false);
-        mAlwaysDiscoverable.stop();
-        if (mBluetoothAdapter.isDiscovering()) {
-            mBluetoothAdapter.cancelDiscovery();
-        }
-    }
-
-    @Override
-    public void onScanningStateChanged(boolean started) {
-        LOG.d("onScanningStateChanged started: " + started + " mIsScanningEnabled: "
-                + mIsScanningEnabled);
-        if (!started && mIsScanningEnabled) {
-            enableScanning();
-        }
-    }
-
-    @Override
-    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
-        LOG.d("onDeviceBondStateChanged device: " + cachedDevice + " state: " + bondState);
-        refreshUi();
-    }
-
-    /**
-     * Helper class to keep the {@link BluetoothAdapter} in discoverable mode indefinitely. By
-     * default, setting the scan mode to BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE will
-     * timeout, but for pairing, we want to keep the device discoverable as long as the page is
-     * scanning.
-     */
-    private static final class AlwaysDiscoverable extends BroadcastReceiver {
-
-        private final Context mContext;
-        private final BluetoothAdapter mAdapter;
-        private final IntentFilter mIntentFilter = new IntentFilter(
-                BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
-
-        private boolean mStarted;
-
-        AlwaysDiscoverable(Context context, BluetoothAdapter adapter) {
-            mContext = context;
-            mAdapter = adapter;
-        }
-
-        /**
-         * Sets the adapter scan mode to
-         * {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE_DISCOVERABLE}. {@link #start()} calls
-         * should have a matching calls to {@link #stop()} when discover mode is no longer needed.
-         */
-        void start() {
-            if (mStarted) {
-                return;
-            }
-            mContext.registerReceiver(this, mIntentFilter);
-            mStarted = true;
-            setDiscoverable();
-        }
-
-        void stop() {
-            if (!mStarted) {
-                return;
-            }
-            mContext.unregisterReceiver(this);
-            mStarted = false;
-            mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            setDiscoverable();
-        }
-
-        private void setDiscoverable() {
-            if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-                mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
-            }
-        }
-    }
 }
diff --git a/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothScanningDevicesGroupPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothScanningDevicesGroupPreferenceControllerTest.java
new file mode 100644
index 0000000..3f62452
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothScanningDevicesGroupPreferenceControllerTest.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 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 android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.LogicalPreferenceGroup;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
+import com.android.car.settings.testutils.ShadowBluetoothAdapter;
+import com.android.car.settings.testutils.ShadowBluetoothPan;
+import com.android.car.settings.testutils.ShadowCarUserManagerHelper;
+import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.Collections;
+
+/** Unit test for {@link BluetoothScanningDevicesGroupPreferenceController}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowCarUserManagerHelper.class, ShadowBluetoothAdapter.class,
+        ShadowBluetoothPan.class})
+public class BluetoothScanningDevicesGroupPreferenceControllerTest {
+
+    @Mock
+    private CarUserManagerHelper mCarUserManagerHelper;
+    @Mock
+    private CachedBluetoothDevice mCachedDevice;
+    @Mock
+    private BluetoothDevice mDevice;
+    @Mock
+    private CachedBluetoothDeviceManager mCachedDeviceManager;
+    private CachedBluetoothDeviceManager mSaveRealCachedDeviceManager;
+    private LocalBluetoothManager mLocalBluetoothManager;
+    private Context mContext;
+    private PreferenceGroup mPreferenceGroup;
+    private PreferenceControllerTestHelper<TestBluetoothScanningDevicesGroupPreferenceController>
+            mControllerHelper;
+    private TestBluetoothScanningDevicesGroupPreferenceController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowCarUserManagerHelper.setMockInstance(mCarUserManagerHelper);
+        mContext = RuntimeEnvironment.application;
+
+        mLocalBluetoothManager = LocalBluetoothManager.getInstance(mContext, /* onInitCallback= */
+                null);
+        mSaveRealCachedDeviceManager = mLocalBluetoothManager.getCachedDeviceManager();
+        ReflectionHelpers.setField(mLocalBluetoothManager, "mCachedDeviceManager",
+                mCachedDeviceManager);
+
+        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+        when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+        when(mCachedDevice.getDevice()).thenReturn(mDevice);
+        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
+                Collections.singletonList(mCachedDevice));
+
+        // Make sure controller is available.
+        Shadows.shadowOf(mContext.getPackageManager()).setSystemFeature(
+                FEATURE_BLUETOOTH, /* supported= */ true);
+        BluetoothAdapter.getDefaultAdapter().enable();
+        getShadowBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
+
+        mPreferenceGroup = new LogicalPreferenceGroup(mContext);
+        mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+                TestBluetoothScanningDevicesGroupPreferenceController.class, mPreferenceGroup);
+        mController = mControllerHelper.getController();
+    }
+
+    @After
+    public void tearDown() {
+        ShadowCarUserManagerHelper.reset();
+        ShadowBluetoothAdapter.reset();
+        ReflectionHelpers.setField(mLocalBluetoothManager, "mCachedDeviceManager",
+                mSaveRealCachedDeviceManager);
+    }
+
+    @Test
+    public void disallowConfigBluetooth_doesNotStartScanning() {
+        when(mCarUserManagerHelper.isCurrentProcessUserHasRestriction(
+                DISALLOW_CONFIG_BLUETOOTH)).thenReturn(true);
+
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
+        // User can't scan, but they can still see known devices.
+        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void onScanningStateChanged_scanningEnabled_receiveStopped_restartsScanning() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+
+        BluetoothAdapter.getDefaultAdapter().cancelDiscovery();
+        mController.onScanningStateChanged(/* started= */ false);
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+    }
+
+    @Test
+    public void onScanningStateChanged_scanningDisabled_receiveStopped_doesNothing() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        // Set a device bonding to disable scanning.
+        when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
+        mController.refreshUi();
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
+
+        mController.onScanningStateChanged(/* started= */ false);
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
+    }
+
+    @Test
+    public void onDeviceBondStateChanged_refreshesUi() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+
+        // Change state to bonding to cancel scanning on refresh.
+        when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
+        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
+        mController.onDeviceBondStateChanged(mCachedDevice, BluetoothDevice.BOND_BONDING);
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
+    }
+
+    @Test
+    public void onDeviceClicked_callsInternal() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        BluetoothDevicePreference devicePreference =
+                (BluetoothDevicePreference) mPreferenceGroup.getPreference(0);
+
+        devicePreference.performClick();
+
+        assertThat(mController.getLastClickedDevice()).isEquivalentAccordingToCompareTo(
+                devicePreference.getCachedDevice());
+    }
+
+    @Test
+    public void onDeviceClicked_cancelsScanning() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        BluetoothDevicePreference devicePreference =
+                (BluetoothDevicePreference) mPreferenceGroup.getPreference(0);
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+
+        devicePreference.performClick();
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
+    }
+
+    @Test
+    public void refreshUi_noDeviceBonding_startsScanning() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+
+        mController.refreshUi();
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+    }
+
+    @Test
+    public void refreshUi_noDeviceBonding_enablesGroup() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+
+        mController.refreshUi();
+
+        assertThat(mPreferenceGroup.isEnabled()).isTrue();
+    }
+
+    @Test
+    public void refreshUi_noDeviceBonding_setsScanModeConnectableDiscoverable() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+
+        mController.refreshUi();
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+    }
+
+    @Test
+    public void refreshUi_deviceBonding_stopsScanning() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
+
+        mController.refreshUi();
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
+    }
+
+    @Test
+    public void refreshUi_deviceBonding_disablesGroup() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
+
+        mController.refreshUi();
+
+        assertThat(mPreferenceGroup.isEnabled()).isFalse();
+    }
+
+    @Test
+    public void refreshUi_deviceBonding_setsScanModeConnectable() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
+
+        mController.refreshUi();
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+    }
+
+    @Test
+    public void onStop_stopsScanning() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
+    }
+
+    @Test
+    public void onStop_clearsNonBondedDevices() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        verify(mCachedDeviceManager).clearNonBondedDevices();
+    }
+
+    @Test
+    public void onStop_clearsGroup() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        assertThat(mPreferenceGroup.getPreferenceCount()).isGreaterThan(0);
+
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void onStop_setsScanModeConnectable() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+    }
+
+    @Test
+    public void discoverableScanModeTimeout_controllerStarted_resetsDiscoverableScanMode() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+
+        BluetoothAdapter.getDefaultAdapter().setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+        mContext.sendBroadcast(new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+    }
+
+    @Test
+    public void discoverableScanModeTimeout_controllerStopped_doesNotResetDiscoverableScanMode() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        BluetoothAdapter.getDefaultAdapter().setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+        mContext.sendBroadcast(new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
+
+        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+    }
+
+    private ShadowBluetoothAdapter getShadowBluetoothAdapter() {
+        return (ShadowBluetoothAdapter) Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+    }
+
+    private static final class TestBluetoothScanningDevicesGroupPreferenceController extends
+            BluetoothScanningDevicesGroupPreferenceController {
+
+        private CachedBluetoothDevice mLastClickedDevice;
+
+        TestBluetoothScanningDevicesGroupPreferenceController(Context context,
+                String preferenceKey,
+                FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+            super(context, preferenceKey, fragmentController, uxRestrictions);
+        }
+
+        @Override
+        protected void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice) {
+            mLastClickedDevice = cachedDevice;
+        }
+
+        CachedBluetoothDevice getLastClickedDevice() {
+            return mLastClickedDevice;
+        }
+
+        @Override
+        protected BluetoothDeviceFilter.Filter getDeviceFilter() {
+            return BluetoothDeviceFilter.ALL_FILTER;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceControllerTest.java
index 28e2b0d..8065703 100644
--- a/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothUnbondedDevicesPreferenceControllerTest.java
@@ -31,7 +31,6 @@
 import android.bluetooth.BluetoothDevice;
 import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
-import android.content.Intent;
 
 import androidx.lifecycle.Lifecycle;
 import androidx.preference.PreferenceGroup;
@@ -76,19 +75,17 @@
     private CachedBluetoothDeviceManager mCachedDeviceManager;
     private CachedBluetoothDeviceManager mSaveRealCachedDeviceManager;
     private LocalBluetoothManager mLocalBluetoothManager;
-    private Context mContext;
     private PreferenceGroup mPreferenceGroup;
     private PreferenceControllerTestHelper<BluetoothUnbondedDevicesPreferenceController>
             mControllerHelper;
-    private BluetoothUnbondedDevicesPreferenceController mController;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         ShadowCarUserManagerHelper.setMockInstance(mCarUserManagerHelper);
-        mContext = RuntimeEnvironment.application;
+        Context context = RuntimeEnvironment.application;
 
-        mLocalBluetoothManager = LocalBluetoothManager.getInstance(mContext, /* onInitCallback= */
+        mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */
                 null);
         mSaveRealCachedDeviceManager = mLocalBluetoothManager.getCachedDeviceManager();
         ReflectionHelpers.setField(mLocalBluetoothManager, "mCachedDeviceManager",
@@ -105,15 +102,14 @@
                 Arrays.asList(mUnbondedCachedDevice, bondedCachedDevice));
 
         // Make sure controller is available.
-        Shadows.shadowOf(mContext.getPackageManager()).setSystemFeature(
+        Shadows.shadowOf(context.getPackageManager()).setSystemFeature(
                 FEATURE_BLUETOOTH, /* supported= */ true);
         BluetoothAdapter.getDefaultAdapter().enable();
         getShadowBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
 
-        mPreferenceGroup = new LogicalPreferenceGroup(mContext);
-        mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+        mPreferenceGroup = new LogicalPreferenceGroup(context);
+        mControllerHelper = new PreferenceControllerTestHelper<>(context,
                 BluetoothUnbondedDevicesPreferenceController.class, mPreferenceGroup);
-        mController = mControllerHelper.getController();
     }
 
     @After
@@ -134,43 +130,6 @@
     }
 
     @Test
-    public void onScanningStateChanged_scanningEnabled_receiveStopped_restartsScanning() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
-
-        BluetoothAdapter.getDefaultAdapter().cancelDiscovery();
-        mController.onScanningStateChanged(/* started= */ false);
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
-    }
-
-    @Test
-    public void onScanningStateChanged_scanningDisabled_receiveStopped_doesNothing() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        // Set a device bonding to disable scanning.
-        when(mUnbondedCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-        mController.refreshUi();
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
-
-        mController.onScanningStateChanged(/* started= */ false);
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
-    }
-
-    @Test
-    public void onDeviceBondStateChanged_refreshesUi() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(1);
-
-        // Bond the only unbonded device.
-        when(mUnbondedCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(mUnbondedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        mController.onDeviceBondStateChanged(mUnbondedCachedDevice, BluetoothDevice.BOND_BONDED);
-
-        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(0);
-    }
-
-    @Test
     public void onDeviceClicked_startsPairing() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
         BluetoothDevicePreference devicePreference =
@@ -182,20 +141,7 @@
     }
 
     @Test
-    public void onDeviceClicked_pairingStarted_cancelsScanning() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        BluetoothDevicePreference devicePreference =
-                (BluetoothDevicePreference) mPreferenceGroup.getPreference(0);
-        when(mUnbondedCachedDevice.startPairing()).thenReturn(true);
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
-
-        devicePreference.performClick();
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
-    }
-
-    @Test
-    public void onDeviceClicked_pairingStartFails_resumeScanning() {
+    public void onDeviceClicked_pairingStartFails_resumesScanning() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
         BluetoothDevicePreference devicePreference =
                 (BluetoothDevicePreference) mPreferenceGroup.getPreference(0);
@@ -221,7 +167,7 @@
     }
 
     @Test
-    public void onDeviceClicked_requests_messageAccess() {
+    public void onDeviceClicked_requestsMessageAccess() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
         when(mUnbondedCachedDevice.startPairing()).thenReturn(true);
         BluetoothDevicePreference devicePreference =
@@ -242,125 +188,6 @@
                 DISABLED_FOR_USER);
     }
 
-    @Test
-    public void refreshUi_noDeviceBonding_startsScanning() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-
-        mController.refreshUi();
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
-    }
-
-    @Test
-    public void refreshUi_noDeviceBonding_enablesGroup() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-
-        mController.refreshUi();
-
-        assertThat(mPreferenceGroup.isEnabled()).isTrue();
-    }
-
-    @Test
-    public void refreshUi_noDeviceBonding_setsScanModeConnectableDiscoverable() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-
-        mController.refreshUi();
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
-                BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
-    }
-
-    @Test
-    public void refreshUi_deviceBonding_stopsScanning() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        when(mUnbondedCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
-        mController.refreshUi();
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
-    }
-
-    @Test
-    public void refreshUi_deviceBonding_disablesGroup() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        when(mUnbondedCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
-        mController.refreshUi();
-
-        assertThat(mPreferenceGroup.isEnabled()).isFalse();
-    }
-
-    @Test
-    public void refreshUi_deviceBonding_setsScanModeConnectable() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        when(mUnbondedCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
-        mController.refreshUi();
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
-                BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-    }
-
-    @Test
-    public void onStop_stopsScanning() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
-
-        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isFalse();
-    }
-
-    @Test
-    public void onStop_clearsNonBondedDevices() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
-
-        verify(mCachedDeviceManager).clearNonBondedDevices();
-    }
-
-    @Test
-    public void onStop_clearsGroup() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        assertThat(mPreferenceGroup.getPreferenceCount()).isGreaterThan(0);
-
-        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
-
-        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(0);
-    }
-
-    @Test
-    public void onStop_setsScanModeConnectable() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
-                BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-    }
-
-    @Test
-    public void discoverableScanModeTimeout_controllerStarted_resetsDiscoverableScanMode() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-
-        BluetoothAdapter.getDefaultAdapter().setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-        mContext.sendBroadcast(new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
-                BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
-    }
-
-    @Test
-    public void discoverableScanModeTimeout_controllerStopped_doesNotResetDiscoverableScanMode() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
-
-        BluetoothAdapter.getDefaultAdapter().setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-        mContext.sendBroadcast(new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
-
-        assertThat(BluetoothAdapter.getDefaultAdapter().getScanMode()).isEqualTo(
-                BluetoothAdapter.SCAN_MODE_CONNECTABLE);
-    }
-
     private ShadowBluetoothAdapter getShadowBluetoothAdapter() {
         return (ShadowBluetoothAdapter) Shadow.extract(BluetoothAdapter.getDefaultAdapter());
     }