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