1 /* 2 * Copyright (C) 2022 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 18 package com.android.server.companion.devicepresence; 19 20 import static android.bluetooth.BluetoothAdapter.ACTION_BLE_STATE_CHANGED; 21 import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED; 22 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES; 23 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_FIRST_MATCH; 24 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST; 25 import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER; 26 27 import static java.util.Objects.requireNonNull; 28 29 import android.annotation.MainThread; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.annotation.SuppressLint; 33 import android.bluetooth.BluetoothAdapter; 34 import android.bluetooth.BluetoothDevice; 35 import android.bluetooth.le.BluetoothLeScanner; 36 import android.bluetooth.le.ScanCallback; 37 import android.bluetooth.le.ScanFilter; 38 import android.bluetooth.le.ScanResult; 39 import android.bluetooth.le.ScanSettings; 40 import android.companion.AssociationInfo; 41 import android.content.BroadcastReceiver; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.content.IntentFilter; 45 import android.os.Handler; 46 import android.os.Looper; 47 import android.util.Slog; 48 49 import com.android.server.companion.association.AssociationStore; 50 import com.android.server.companion.association.AssociationStore.ChangeType; 51 52 import java.util.ArrayList; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Set; 56 57 @SuppressLint("LongLogTag") 58 class BleDeviceProcessor implements AssociationStore.OnChangeListener { 59 private static final String TAG = "CDM_BleDeviceProcessor"; 60 61 interface Callback { onBleCompanionDeviceFound(int associationId, int userId)62 void onBleCompanionDeviceFound(int associationId, int userId); 63 onBleCompanionDeviceLost(int associationId, int userId)64 void onBleCompanionDeviceLost(int associationId, int userId); 65 } 66 67 @NonNull 68 private final AssociationStore mAssociationStore; 69 @NonNull 70 private final Callback mCallback; 71 72 // Non-null after init(). 73 @Nullable 74 private BluetoothAdapter mBtAdapter; 75 // Non-null after init() and when BLE is available. Otherwise - null. 76 @Nullable 77 private BluetoothLeScanner mBleScanner; 78 // Only accessed from the Main thread. 79 private boolean mScanning = false; 80 BleDeviceProcessor(@onNull AssociationStore associationStore, @NonNull Callback callback)81 BleDeviceProcessor(@NonNull AssociationStore associationStore, @NonNull Callback callback) { 82 mAssociationStore = associationStore; 83 mCallback = callback; 84 } 85 86 @MainThread init(@onNull Context context, @NonNull BluetoothAdapter btAdapter)87 void init(@NonNull Context context, @NonNull BluetoothAdapter btAdapter) { 88 if (mBtAdapter != null) { 89 throw new IllegalStateException(getClass().getSimpleName() + " is already initialized"); 90 } 91 mBtAdapter = requireNonNull(btAdapter); 92 93 checkBleState(); 94 registerBluetoothStateBroadcastReceiver(context); 95 96 mAssociationStore.registerLocalListener(this); 97 } 98 99 @MainThread restartScan()100 final void restartScan() { 101 enforceInitialized(); 102 103 if (mBleScanner == null) { 104 return; 105 } 106 107 stopScanIfNeeded(); 108 startScan(); 109 } 110 111 @Override onAssociationChanged(@hangeType int changeType, AssociationInfo association)112 public void onAssociationChanged(@ChangeType int changeType, AssociationInfo association) { 113 // Simply restart scanning. 114 if (Looper.getMainLooper().isCurrentThread()) { 115 restartScan(); 116 } else { 117 new Handler(Looper.getMainLooper()).post(this::restartScan); 118 } 119 } 120 121 @MainThread checkBleState()122 private void checkBleState() { 123 enforceInitialized(); 124 125 final boolean bleAvailable = mBtAdapter.isLeEnabled(); 126 if ((bleAvailable && mBleScanner != null) || (!bleAvailable && mBleScanner == null)) { 127 // Nothing changed. 128 return; 129 } 130 131 if (bleAvailable) { 132 mBleScanner = mBtAdapter.getBluetoothLeScanner(); 133 if (mBleScanner == null) { 134 // Oops, that's a race condition. Can return. 135 return; 136 } 137 138 startScan(); 139 } else { 140 stopScanIfNeeded(); 141 mBleScanner = null; 142 } 143 } 144 145 @MainThread startScan()146 void startScan() { 147 enforceInitialized(); 148 149 Slog.i(TAG, "startBleScan()"); 150 // This method should not be called if scan is already in progress. 151 if (mScanning) { 152 Slog.w(TAG, "Scan is already in progress."); 153 return; 154 } 155 156 // Neither should this method be called if the adapter is not available. 157 if (mBleScanner == null) { 158 Slog.w(TAG, "BLE is not available."); 159 return; 160 } 161 162 // Collect MAC addresses from all associations. 163 final Set<String> macAddresses = new HashSet<>(); 164 for (AssociationInfo association : mAssociationStore.getActiveAssociations()) { 165 if (!association.isNotifyOnDeviceNearby()) continue; 166 167 // Beware that BT stack does not consider low-case MAC addresses valid, while 168 // MacAddress.toString() return a low-case String. 169 final String macAddress = association.getDeviceMacAddressAsString(); 170 if (macAddress != null) { 171 macAddresses.add(macAddress); 172 } 173 } 174 if (macAddresses.isEmpty()) { 175 return; 176 } 177 178 final List<ScanFilter> filters = new ArrayList<>(macAddresses.size()); 179 for (String macAddress : macAddresses) { 180 final ScanFilter filter = new ScanFilter.Builder() 181 .setDeviceAddress(macAddress) 182 .build(); 183 filters.add(filter); 184 } 185 186 // BluetoothLeScanner will throw an IllegalStateException if startScan() is called while LE 187 // is not enabled. 188 if (mBtAdapter.isLeEnabled()) { 189 try { 190 mBleScanner.startScan(filters, SCAN_SETTINGS, mScanCallback); 191 mScanning = true; 192 } catch (IllegalStateException e) { 193 Slog.w(TAG, "Exception while starting BLE scanning", e); 194 } 195 } else { 196 Slog.w(TAG, "BLE scanning is not turned on"); 197 } 198 } 199 stopScanIfNeeded()200 void stopScanIfNeeded() { 201 enforceInitialized(); 202 203 Slog.i(TAG, "stopBleScan()"); 204 if (!mScanning) { 205 return; 206 } 207 // mScanCallback is non-null here - it cannot be null when mScanning is true. 208 209 // BluetoothLeScanner will throw an IllegalStateException if stopScan() is called while LE 210 // is not enabled. 211 if (mBtAdapter.isLeEnabled()) { 212 try { 213 mBleScanner.stopScan(mScanCallback); 214 } catch (IllegalStateException e) { 215 Slog.w(TAG, "Exception while stopping BLE scanning", e); 216 } 217 } else { 218 Slog.w(TAG, "BLE scanning is not turned on"); 219 } 220 221 mScanning = false; 222 } 223 224 @MainThread notifyDeviceFound(@onNull BluetoothDevice device)225 private void notifyDeviceFound(@NonNull BluetoothDevice device) { 226 for (AssociationInfo association : mAssociationStore.getActiveAssociationsByAddress( 227 device.getAddress())) { 228 mCallback.onBleCompanionDeviceFound(association.getId(), association.getUserId()); 229 } 230 } 231 232 @MainThread notifyDeviceLost(@onNull BluetoothDevice device)233 private void notifyDeviceLost(@NonNull BluetoothDevice device) { 234 for (AssociationInfo association : mAssociationStore.getActiveAssociationsByAddress( 235 device.getAddress())) { 236 mCallback.onBleCompanionDeviceLost(association.getId(), association.getUserId()); 237 } 238 } 239 registerBluetoothStateBroadcastReceiver(Context context)240 private void registerBluetoothStateBroadcastReceiver(Context context) { 241 final BroadcastReceiver receiver = new BroadcastReceiver() { 242 @Override 243 public void onReceive(Context context, Intent intent) { 244 // Post to the main thread to make sure it is a Non-Blocking call. 245 new Handler(Looper.getMainLooper()).post(() -> checkBleState()); 246 } 247 }; 248 249 final IntentFilter filter = new IntentFilter(); 250 filter.addAction(ACTION_STATE_CHANGED); 251 filter.addAction(ACTION_BLE_STATE_CHANGED); 252 253 context.registerReceiver(receiver, filter); 254 } 255 enforceInitialized()256 private void enforceInitialized() { 257 if (mBtAdapter != null) return; 258 throw new IllegalStateException(getClass().getSimpleName() + " is not initialized"); 259 } 260 261 private final ScanCallback mScanCallback = new ScanCallback() { 262 @MainThread 263 @Override 264 public void onScanResult(int callbackType, ScanResult result) { 265 final BluetoothDevice device = result.getDevice(); 266 267 switch (callbackType) { 268 case CALLBACK_TYPE_FIRST_MATCH: 269 notifyDeviceFound(device); 270 break; 271 272 case CALLBACK_TYPE_MATCH_LOST: 273 notifyDeviceLost(device); 274 break; 275 276 default: 277 Slog.wtf(TAG, "Unexpected callback " 278 + nameForBleScanCallbackType(callbackType)); 279 break; 280 } 281 } 282 283 @MainThread 284 @Override 285 public void onScanFailed(int errorCode) { 286 mScanning = false; 287 } 288 }; 289 nameForBleScanCallbackType(int callbackType)290 private static String nameForBleScanCallbackType(int callbackType) { 291 final String name = switch (callbackType) { 292 case CALLBACK_TYPE_ALL_MATCHES -> "ALL_MATCHES"; 293 case CALLBACK_TYPE_FIRST_MATCH -> "FIRST_MATCH"; 294 case CALLBACK_TYPE_MATCH_LOST -> "MATCH_LOST"; 295 default -> "Unknown"; 296 }; 297 return name + "(" + callbackType + ")"; 298 } 299 300 private static final ScanSettings SCAN_SETTINGS = new ScanSettings.Builder() 301 .setCallbackType(CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST) 302 .setScanMode(SCAN_MODE_LOW_POWER) 303 .build(); 304 } 305