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