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