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.car.drivingstate.CarUxRestrictions;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 
29 import androidx.preference.PreferenceGroup;
30 
31 import com.android.car.settings.common.FragmentController;
32 import com.android.car.settings.common.Logger;
33 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
34 
35 /**
36  * Controller which sets the Bluetooth adapter to discovery mode and begins scanning for
37  * discoverable devices for as long as the preference group is shown. Discovery
38  * and scanning are halted while any device is pairing. Users with the {@link
39  * DISALLOW_CONFIG_BLUETOOTH} restriction cannot scan for devices, so only cached devices will be
40  * shown.
41  */
42 public abstract class BluetoothScanningDevicesGroupPreferenceController extends
43         BluetoothDevicesGroupPreferenceController {
44 
45     private static final Logger LOG = new Logger(
46             BluetoothScanningDevicesGroupPreferenceController.class);
47 
48     private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
49     private final AlwaysDiscoverable mAlwaysDiscoverable;
50     private boolean mIsScanningEnabled;
51 
BluetoothScanningDevicesGroupPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)52     public BluetoothScanningDevicesGroupPreferenceController(Context context, String preferenceKey,
53             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
54         super(context, preferenceKey, fragmentController, uxRestrictions);
55         mAlwaysDiscoverable = new AlwaysDiscoverable(context, mBluetoothAdapter);
56     }
57 
58     @Override
onDeviceClicked(CachedBluetoothDevice cachedDevice)59     protected final void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
60         LOG.d("onDeviceClicked: " + cachedDevice);
61         disableScanning();
62         onDeviceClickedInternal(cachedDevice);
63     }
64 
65     /**
66      * Called when the user selects a device in the group.
67      *
68      * @param cachedDevice the device represented by the selected preference.
69      */
onDeviceClickedInternal(CachedBluetoothDevice cachedDevice)70     protected abstract void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice);
71 
72     @Override
onStopInternal()73     protected void onStopInternal() {
74         super.onStopInternal();
75         disableScanning();
76         getBluetoothManager().getCachedDeviceManager().clearNonBondedDevices();
77         getPreferenceMap().clear();
78         getPreference().removeAll();
79     }
80 
81     @Override
updateState(PreferenceGroup preferenceGroup)82     protected void updateState(PreferenceGroup preferenceGroup) {
83         super.updateState(preferenceGroup);
84         if (shouldEnableScanning()) {
85             enableScanning();
86         } else {
87             disableScanning();
88         }
89     }
90 
shouldEnableScanning()91     private boolean shouldEnableScanning() {
92         for (CachedBluetoothDevice device : getPreferenceMap().keySet()) {
93             if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
94                 return false;
95             }
96         }
97         // Users who cannot configure Bluetooth cannot scan.
98         return !getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
99     }
100 
101     /**
102      * Starts scanning for devices which will be displayed in the group for a user to select.
103      * Calls are idempotent.
104      */
enableScanning()105     private void enableScanning() {
106         mIsScanningEnabled = true;
107         if (!mBluetoothAdapter.isDiscovering()) {
108             mBluetoothAdapter.startDiscovery();
109         }
110         mAlwaysDiscoverable.start();
111         getPreference().setEnabled(true);
112     }
113 
114     /** Stops scanning for devices and disables interaction. Calls are idempotent. */
disableScanning()115     private void disableScanning() {
116         mIsScanningEnabled = false;
117         getPreference().setEnabled(false);
118         mAlwaysDiscoverable.stop();
119         if (mBluetoothAdapter.isDiscovering()) {
120             mBluetoothAdapter.cancelDiscovery();
121         }
122     }
123 
124     @Override
onScanningStateChanged(boolean started)125     public void onScanningStateChanged(boolean started) {
126         LOG.d("onScanningStateChanged started: " + started + " mIsScanningEnabled: "
127                 + mIsScanningEnabled);
128         if (!started && mIsScanningEnabled) {
129             enableScanning();
130         }
131     }
132 
133     @Override
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)134     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
135         LOG.d("onDeviceBondStateChanged device: " + cachedDevice + " state: " + bondState);
136         refreshUi();
137     }
138 
139     /**
140      * Helper class to keep the {@link BluetoothAdapter} in discoverable mode indefinitely. By
141      * default, setting the scan mode to BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE will
142      * timeout, but for pairing, we want to keep the device discoverable as long as the page is
143      * scanning.
144      */
145     private static final class AlwaysDiscoverable extends BroadcastReceiver {
146 
147         private final Context mContext;
148         private final BluetoothAdapter mAdapter;
149         private final IntentFilter mIntentFilter = new IntentFilter(
150                 BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
151 
152         private boolean mStarted;
153 
AlwaysDiscoverable(Context context, BluetoothAdapter adapter)154         AlwaysDiscoverable(Context context, BluetoothAdapter adapter) {
155             mContext = context;
156             mAdapter = adapter;
157         }
158 
159         /**
160          * Sets the adapter scan mode to
161          * {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE_DISCOVERABLE}. {@link #start()} calls
162          * should have a matching calls to {@link #stop()} when discover mode is no longer needed.
163          */
start()164         void start() {
165             if (mStarted) {
166                 return;
167             }
168             mContext.registerReceiver(this, mIntentFilter);
169             mStarted = true;
170             setDiscoverable();
171         }
172 
stop()173         void stop() {
174             if (!mStarted) {
175                 return;
176             }
177             mContext.unregisterReceiver(this);
178             mStarted = false;
179             mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
180         }
181 
182         @Override
onReceive(Context context, Intent intent)183         public void onReceive(Context context, Intent intent) {
184             setDiscoverable();
185         }
186 
setDiscoverable()187         private void setDiscoverable() {
188             if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
189                 mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
190             }
191         }
192     }
193 }
194