1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.settings.bluetooth;
18 
19 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
20 
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothManager;
24 import android.car.drivingstate.CarUxRestrictions;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 
30 import androidx.preference.PreferenceGroup;
31 
32 import com.android.car.settings.common.FragmentController;
33 import com.android.car.settings.common.Logger;
34 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
35 
36 /**
37  * Controller which sets the Bluetooth adapter to discovery mode and begins scanning for
38  * discoverable devices for as long as the preference group is shown. Discovery
39  * and scanning are halted while any device is pairing. Users with the {@link
40  * DISALLOW_CONFIG_BLUETOOTH} restriction cannot scan for devices, so only cached devices will be
41  * shown.
42  */
43 public abstract class BluetoothScanningDevicesGroupPreferenceController extends
44         BluetoothDevicesGroupPreferenceController {
45 
46     private static final Logger LOG = new Logger(
47             BluetoothScanningDevicesGroupPreferenceController.class);
48 
49     protected final BluetoothAdapter mBluetoothAdapter;
50     private final AlwaysDiscoverable mAlwaysDiscoverable;
51     private boolean mIsScanningEnabled;
52 
BluetoothScanningDevicesGroupPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)53     public BluetoothScanningDevicesGroupPreferenceController(Context context, String preferenceKey,
54             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
55         super(context, preferenceKey, fragmentController, uxRestrictions);
56         mBluetoothAdapter = getContext().getSystemService(BluetoothManager.class).getAdapter();
57         mAlwaysDiscoverable = new AlwaysDiscoverable(context, mBluetoothAdapter);
58     }
59 
60     @Override
onDeviceClicked(CachedBluetoothDevice cachedDevice)61     protected final void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
62         LOG.d("onDeviceClicked: " + cachedDevice);
63         disableScanning();
64         onDeviceClickedInternal(cachedDevice);
65     }
66 
67     /**
68      * Called when the user selects a device in the group.
69      *
70      * @param cachedDevice the device represented by the selected preference.
71      */
onDeviceClickedInternal(CachedBluetoothDevice cachedDevice)72     protected abstract void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice);
73 
74     @Override
onStartInternal()75     protected void onStartInternal() {
76         super.onStartInternal();
77         mIsScanningEnabled = true;
78     }
79 
80     @Override
onStopInternal()81     protected void onStopInternal() {
82         super.onStopInternal();
83         disableScanning();
84         getBluetoothManager().getCachedDeviceManager().clearNonBondedDevices();
85         getPreferenceMap().clear();
86         getPreference().removeAll();
87     }
88 
89     @Override
updateState(PreferenceGroup preferenceGroup)90     protected void updateState(PreferenceGroup preferenceGroup) {
91         super.updateState(preferenceGroup);
92         if (shouldEnableScanning() && mIsScanningEnabled) {
93             enableScanning();
94         } else {
95             disableScanning();
96         }
97     }
98 
reenableScanning()99     protected void reenableScanning() {
100         if (isStarted()) {
101             mIsScanningEnabled = true;
102         }
103         refreshUi();
104     }
105 
shouldEnableScanning()106     private boolean shouldEnableScanning() {
107         for (CachedBluetoothDevice device : getPreferenceMap().keySet()) {
108             if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
109                 return false;
110             }
111         }
112         // Users who cannot configure Bluetooth cannot scan.
113         return !getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
114     }
115 
116     /**
117      * Starts scanning for devices which will be displayed in the group for a user to select.
118      * Calls are idempotent.
119      */
enableScanning()120     private void enableScanning() {
121         mIsScanningEnabled = true;
122         if (!mBluetoothAdapter.isDiscovering()) {
123             mBluetoothAdapter.startDiscovery();
124         }
125         mAlwaysDiscoverable.start();
126         getPreference().setEnabled(true);
127     }
128 
129     /** Stops scanning for devices and disables interaction. Calls are idempotent. */
disableScanning()130     private void disableScanning() {
131         mIsScanningEnabled = false;
132         getPreference().setEnabled(false);
133         mAlwaysDiscoverable.stop();
134         if (mBluetoothAdapter.isDiscovering()) {
135             mBluetoothAdapter.cancelDiscovery();
136         }
137     }
138 
139     @Override
onScanningStateChanged(boolean started)140     public void onScanningStateChanged(boolean started) {
141         LOG.d("onScanningStateChanged started: " + started + " mIsScanningEnabled: "
142                 + mIsScanningEnabled);
143         if (!started && mIsScanningEnabled) {
144             enableScanning();
145         }
146     }
147 
148     @Override
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)149     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
150         LOG.d("onDeviceBondStateChanged device: " + cachedDevice + " state: " + bondState);
151         if (bondState == BluetoothDevice.BOND_NONE && isStarted()) {
152             mIsScanningEnabled = true;
153         }
154         refreshUi();
155     }
156 
157     /**
158      * Helper class to keep the {@link BluetoothAdapter} in discoverable mode indefinitely. By
159      * default, setting the scan mode to BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE will
160      * timeout, but for pairing, we want to keep the device discoverable as long as the page is
161      * scanning.
162      */
163     private static final class AlwaysDiscoverable extends BroadcastReceiver {
164 
165         private final Context mContext;
166         private final BluetoothAdapter mAdapter;
167         private final IntentFilter mIntentFilter = new IntentFilter(
168                 BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
169 
170         private boolean mStarted;
171 
AlwaysDiscoverable(Context context, BluetoothAdapter adapter)172         AlwaysDiscoverable(Context context, BluetoothAdapter adapter) {
173             mContext = context;
174             mAdapter = adapter;
175         }
176 
177         /**
178          * Sets the adapter scan mode to
179          * {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE_DISCOVERABLE}. {@link #start()} calls
180          * should have a matching calls to {@link #stop()} when discover mode is no longer needed.
181          */
start()182         void start() {
183             if (mStarted) {
184                 return;
185             }
186             mContext.registerReceiver(this, mIntentFilter);
187             mStarted = true;
188             setDiscoverable();
189         }
190 
stop()191         void stop() {
192             if (!mStarted) {
193                 return;
194             }
195             mContext.unregisterReceiver(this);
196             mStarted = false;
197             mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
198         }
199 
200         @Override
onReceive(Context context, Intent intent)201         public void onReceive(Context context, Intent intent) {
202             setDiscoverable();
203         }
204 
setDiscoverable()205         private void setDiscoverable() {
206             if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
207                 mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
208             }
209         }
210     }
211 }
212