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