diff --git a/src/com/android/settings/bluetooth/AlwaysDiscoverable.java b/src/com/android/settings/bluetooth/AlwaysDiscoverable.java new file mode 100644 index 00000000000..5d7cbd59faa --- /dev/null +++ b/src/com/android/settings/bluetooth/AlwaysDiscoverable.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.settingslib.bluetooth.LocalBluetoothAdapter; + +import java.util.Timer; +import java.util.TimerTask; + +/** Helper class, intended to be used by an Activity, to keep the local Bluetooth adapter in + * discoverable mode indefinitely. By default setting the scan mode to + * BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE will time out after some time, but some + * Bluetooth settings pages would like to keep the device discoverable as long as the page is + * visible. */ +public class AlwaysDiscoverable extends BroadcastReceiver { + private static final String TAG = "AlwaysDiscoverable"; + + private Context mContext; + private LocalBluetoothAdapter mLocalAdapter; + private IntentFilter mIntentFilter; + + @VisibleForTesting + boolean mStarted; + + public AlwaysDiscoverable(Context context, LocalBluetoothAdapter localAdapter) { + mContext = context; + mLocalAdapter = localAdapter; + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); + } + + /** After calling start(), consumers should make a matching call to stop() when they no longer + * wish to enforce discoverable mode. */ + public void start() { + if (mStarted) { + return; + } + mContext.registerReceiver(this, mIntentFilter); + mStarted = true; + if (mLocalAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + } + } + + public void stop() { + if (!mStarted) { + return; + } + mContext.unregisterReceiver(this); + mStarted = false; + mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action != BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) { + return; + } + if (mLocalAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + } + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java index 5cc1060f1ab..60011f9ef20 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java @@ -53,6 +53,8 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme BluetoothProgressCategory mAvailableDevicesCategory; @VisibleForTesting FooterPreference mFooterPreference; + @VisibleForTesting + AlwaysDiscoverable mAlwaysDiscoverable; private boolean mInitialScanStarted; @@ -64,6 +66,7 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mInitialScanStarted = false; + mAlwaysDiscoverable = new AlwaysDiscoverable(getContext(), mLocalAdapter); } @Override @@ -79,7 +82,7 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme super.onStop(); // Make the device only visible to connected devices. - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); + mAlwaysDiscoverable.stop(); disableScanning(); } @@ -132,9 +135,7 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme R.string.bluetooth_preference_found_devices, BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER, mInitialScanStarted); updateFooterPreference(mFooterPreference); - // mLocalAdapter.setScanMode is internally synchronized so it is okay for multiple - // threads to execute. - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + mAlwaysDiscoverable.start(); enableScanning(); break; diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java index adfb721da6c..bd86c4bcad5 100644 --- a/src/com/android/settings/bluetooth/BluetoothSettings.java +++ b/src/com/android/settings/bluetooth/BluetoothSettings.java @@ -80,6 +80,7 @@ public class BluetoothSettings extends DeviceListPreferenceFragment implements I FooterPreference mFooterPreference; private Preference mPairingPreference; private BluetoothEnabler mBluetoothEnabler; + private AlwaysDiscoverable mAlwaysDiscoverable; private SwitchBar mSwitchBar; @@ -111,6 +112,9 @@ public class BluetoothSettings extends DeviceListPreferenceFragment implements I mMetricsFeatureProvider, Utils.getLocalBtManager(activity), MetricsEvent.ACTION_BLUETOOTH_TOGGLE); mBluetoothEnabler.setupSwitchController(); + if (mLocalAdapter != null) { + mAlwaysDiscoverable = new AlwaysDiscoverable(getContext(), mLocalAdapter); + } } @Override @@ -157,7 +161,9 @@ public class BluetoothSettings extends DeviceListPreferenceFragment implements I } // Make the device only visible to connected devices. - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); + if (mAlwaysDiscoverable != null) { + mAlwaysDiscoverable.stop(); + } if (isUiRestricted()) { return; @@ -188,7 +194,9 @@ public class BluetoothSettings extends DeviceListPreferenceFragment implements I mPairedDevicesCategory.addPreference(mPairingPreference); updateFooterPreference(mFooterPreference); - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + if (mAlwaysDiscoverable != null) { + mAlwaysDiscoverable.start(); + } return; // not break case BluetoothAdapter.STATE_TURNING_OFF: diff --git a/tests/robotests/src/com/android/settings/bluetooth/AlwaysDiscoverableTest.java b/tests/robotests/src/com/android/settings/bluetooth/AlwaysDiscoverableTest.java new file mode 100644 index 00000000000..fd46b4b3727 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/AlwaysDiscoverableTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.bluetooth.LocalBluetoothAdapter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class AlwaysDiscoverableTest { + @Mock + private LocalBluetoothAdapter mLocalAdapter; + + @Mock + private Context mContext; + + private AlwaysDiscoverable mAlwaysDiscoverable; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mAlwaysDiscoverable = new AlwaysDiscoverable(mContext, mLocalAdapter); + } + + @Test + public void isStartedWithoutStart() { + assertThat(mAlwaysDiscoverable.mStarted).isFalse(); + } + + @Test + public void isStartedWithStart() { + mAlwaysDiscoverable.start(); + assertThat(mAlwaysDiscoverable.mStarted).isTrue(); + } + + @Test + public void isStartedWithStartStop() { + mAlwaysDiscoverable.start(); + mAlwaysDiscoverable.stop(); + assertThat(mAlwaysDiscoverable.mStarted).isFalse(); + } + + @Test + public void stopWithoutStart() { + mAlwaysDiscoverable.stop(); + // expect no crash + verify(mLocalAdapter, never()).setScanMode(anyInt()); + } + + @Test + public void startSetsModeAndRegistersReceiver() { + when(mLocalAdapter.getScanMode()).thenReturn(BluetoothAdapter.SCAN_MODE_NONE); + mAlwaysDiscoverable.start(); + verify(mLocalAdapter).setScanMode(eq(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)); + verify(mContext).registerReceiver(eq(mAlwaysDiscoverable), any()); + } + + @Test + public void stopUnregistersReceiver() { + mAlwaysDiscoverable.start(); + mAlwaysDiscoverable.stop(); + verify(mContext).unregisterReceiver(mAlwaysDiscoverable); + } + + @Test + public void resetsToDiscoverableModeWhenScanModeChanges() { + mAlwaysDiscoverable.start(); + verify(mLocalAdapter, times(1)).setScanMode( + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + + sendScanModeChangedIntent(BluetoothAdapter.SCAN_MODE_CONNECTABLE, + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + + verify(mLocalAdapter, times(2)).setScanMode( + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + } + + private void sendScanModeChangedIntent(int newMode, int previousMode) { + when(mLocalAdapter.getScanMode()).thenReturn(newMode); + Intent intent = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, newMode); + intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_SCAN_MODE, previousMode); + mAlwaysDiscoverable.onReceive(mContext, intent); + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java index c4772e5e8e3..d1d49354403 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java @@ -85,6 +85,7 @@ public class BluetoothPairingDetailTest { mFragment.mLocalAdapter = mLocalAdapter; mFragment.mLocalManager = mLocalManager; mFragment.mDeviceListGroup = mPreferenceGroup; + mFragment.mAlwaysDiscoverable = new AlwaysDiscoverable(mContext, mLocalAdapter); } @Test