1 /* 2 * Copyright (C) 2013 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.companiondevicemanager; 18 19 import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; 20 import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; 21 22 import static com.android.internal.util.ArrayUtils.isEmpty; 23 import static com.android.internal.util.CollectionUtils.emptyIfNull; 24 import static com.android.internal.util.CollectionUtils.size; 25 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 26 27 import android.annotation.MainThread; 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.app.PendingIntent; 31 import android.app.Service; 32 import android.bluetooth.BluetoothAdapter; 33 import android.bluetooth.BluetoothDevice; 34 import android.bluetooth.BluetoothManager; 35 import android.bluetooth.BluetoothProfile; 36 import android.bluetooth.le.BluetoothLeScanner; 37 import android.bluetooth.le.ScanCallback; 38 import android.bluetooth.le.ScanFilter; 39 import android.bluetooth.le.ScanResult; 40 import android.bluetooth.le.ScanSettings; 41 import android.companion.Association; 42 import android.companion.AssociationRequest; 43 import android.companion.BluetoothDeviceFilter; 44 import android.companion.BluetoothLeDeviceFilter; 45 import android.companion.DeviceFilter; 46 import android.companion.ICompanionDeviceDiscoveryService; 47 import android.companion.IFindDeviceCallback; 48 import android.companion.WifiDeviceFilter; 49 import android.content.BroadcastReceiver; 50 import android.content.Context; 51 import android.content.Intent; 52 import android.content.IntentFilter; 53 import android.net.wifi.WifiManager; 54 import android.os.Handler; 55 import android.os.IBinder; 56 import android.os.Parcelable; 57 import android.os.RemoteException; 58 import android.text.TextUtils; 59 import android.util.Log; 60 61 import com.android.internal.infra.AndroidFuture; 62 import com.android.internal.util.ArrayUtils; 63 import com.android.internal.util.CollectionUtils; 64 import com.android.internal.util.Preconditions; 65 66 import java.util.ArrayList; 67 import java.util.List; 68 import java.util.Objects; 69 70 public class CompanionDeviceDiscoveryService extends Service { 71 72 private static final boolean DEBUG = false; 73 private static final String LOG_TAG = CompanionDeviceDiscoveryService.class.getSimpleName(); 74 75 private static final long SCAN_TIMEOUT = 20000; 76 77 static CompanionDeviceDiscoveryService sInstance; 78 79 private BluetoothManager mBluetoothManager; 80 private BluetoothAdapter mBluetoothAdapter; 81 private WifiManager mWifiManager; 82 @Nullable private BluetoothLeScanner mBLEScanner; 83 private ScanSettings mDefaultScanSettings = new ScanSettings.Builder() 84 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 85 .build(); 86 87 private List<DeviceFilter<?>> mFilters; 88 private List<BluetoothLeDeviceFilter> mBLEFilters; 89 private List<BluetoothDeviceFilter> mBluetoothFilters; 90 private List<WifiDeviceFilter> mWifiFilters; 91 private List<ScanFilter> mBLEScanFilters; 92 93 AssociationRequest mRequest; 94 List<DeviceFilterPair> mDevicesFound; 95 DeviceFilterPair mSelectedDevice; 96 IFindDeviceCallback mFindCallback; 97 98 AndroidFuture<Association> mServiceCallback; 99 boolean mIsScanning = false; 100 @Nullable 101 CompanionDeviceActivity mActivity = null; 102 103 private final ICompanionDeviceDiscoveryService mBinder = 104 new ICompanionDeviceDiscoveryService.Stub() { 105 @Override 106 public void startDiscovery(AssociationRequest request, 107 String callingPackage, 108 IFindDeviceCallback findCallback, 109 AndroidFuture serviceCallback) { 110 Log.i(LOG_TAG, 111 "startDiscovery() called with: filter = [" + request 112 + "], findCallback = [" + findCallback + "]" 113 + "], serviceCallback = [" + serviceCallback + "]"); 114 mFindCallback = findCallback; 115 mServiceCallback = serviceCallback; 116 Handler.getMain().sendMessage(obtainMessage( 117 CompanionDeviceDiscoveryService::startDiscovery, 118 CompanionDeviceDiscoveryService.this, request)); 119 } 120 121 @Override 122 public void onAssociationCreated() { 123 Handler.getMain().post(CompanionDeviceDiscoveryService.this::onAssociationCreated); 124 } 125 }; 126 127 private ScanCallback mBLEScanCallback; 128 private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver; 129 private WifiBroadcastReceiver mWifiBroadcastReceiver; 130 131 @Override onBind(Intent intent)132 public IBinder onBind(Intent intent) { 133 Log.i(LOG_TAG, "onBind(" + intent + ")"); 134 return mBinder.asBinder(); 135 } 136 137 @Override onCreate()138 public void onCreate() { 139 super.onCreate(); 140 141 Log.i(LOG_TAG, "onCreate()"); 142 143 mBluetoothManager = getSystemService(BluetoothManager.class); 144 mBluetoothAdapter = mBluetoothManager.getAdapter(); 145 mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); 146 mWifiManager = getSystemService(WifiManager.class); 147 148 mDevicesFound = new ArrayList<>(); 149 150 sInstance = this; 151 } 152 153 @MainThread startDiscovery(AssociationRequest request)154 private void startDiscovery(AssociationRequest request) { 155 if (!request.equals(mRequest)) { 156 mRequest = request; 157 158 mFilters = request.getDeviceFilters(); 159 mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); 160 mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); 161 mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class); 162 mBLEScanFilters 163 = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter); 164 165 reset(); 166 } else { 167 Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request); 168 } 169 170 if (!ArrayUtils.isEmpty(mDevicesFound)) { 171 onReadyToShowUI(); 172 } 173 174 // If filtering to get single device by mac address, also search in the set of already 175 // bonded devices to allow linking those directly 176 String singleMacAddressFilter = null; 177 if (mRequest.isSingleDevice()) { 178 int numFilters = size(mBluetoothFilters); 179 for (int i = 0; i < numFilters; i++) { 180 BluetoothDeviceFilter filter = mBluetoothFilters.get(i); 181 if (!TextUtils.isEmpty(filter.getAddress())) { 182 singleMacAddressFilter = filter.getAddress(); 183 break; 184 } 185 } 186 } 187 if (singleMacAddressFilter != null) { 188 for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) { 189 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 190 } 191 for (BluetoothDevice dev : emptyIfNull( 192 mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT))) { 193 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 194 } 195 for (BluetoothDevice dev : emptyIfNull( 196 mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER))) { 197 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 198 } 199 } 200 201 if (shouldScan(mBluetoothFilters)) { 202 final IntentFilter intentFilter = new IntentFilter(); 203 intentFilter.addAction(BluetoothDevice.ACTION_FOUND); 204 205 Log.i(LOG_TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)"); 206 mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver(); 207 registerReceiver(mBluetoothBroadcastReceiver, intentFilter); 208 mBluetoothAdapter.startDiscovery(); 209 } 210 211 if (shouldScan(mBLEFilters) && mBLEScanner != null) { 212 Log.i(LOG_TAG, "BLEScanner.startScan"); 213 mBLEScanCallback = new BLEScanCallback(); 214 mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); 215 } 216 217 if (shouldScan(mWifiFilters)) { 218 Log.i(LOG_TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)"); 219 mWifiBroadcastReceiver = new WifiBroadcastReceiver(); 220 registerReceiver(mWifiBroadcastReceiver, 221 new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); 222 mWifiManager.startScan(); 223 } 224 mIsScanning = true; 225 Handler.getMain().sendMessageDelayed( 226 obtainMessage(CompanionDeviceDiscoveryService::stopScan, this), 227 SCAN_TIMEOUT); 228 } 229 230 @MainThread onAssociationCreated()231 private void onAssociationCreated() { 232 mActivity.setResultAndFinish(); 233 } 234 shouldScan(List<? extends DeviceFilter> mediumSpecificFilters)235 private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) { 236 return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters); 237 } 238 239 @MainThread reset()240 private void reset() { 241 Log.i(LOG_TAG, "reset()"); 242 stopScan(); 243 mDevicesFound.clear(); 244 mSelectedDevice = null; 245 CompanionDeviceActivity.notifyDevicesChanged(); 246 } 247 248 @Override onUnbind(Intent intent)249 public boolean onUnbind(Intent intent) { 250 Log.i(LOG_TAG, "onUnbind(intent = " + intent + ")"); 251 stopScan(); 252 return super.onUnbind(intent); 253 } 254 stopScan()255 private void stopScan() { 256 Log.i(LOG_TAG, "stopScan()"); 257 258 if (!mIsScanning) return; 259 mIsScanning = false; 260 261 if (mActivity != null && mActivity.mDeviceListView != null) { 262 mActivity.mDeviceListView.removeFooterView(mActivity.mLoadingIndicator); 263 } 264 265 mBluetoothAdapter.cancelDiscovery(); 266 if (mBluetoothBroadcastReceiver != null) { 267 unregisterReceiver(mBluetoothBroadcastReceiver); 268 mBluetoothBroadcastReceiver = null; 269 } 270 if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback); 271 if (mWifiBroadcastReceiver != null) { 272 unregisterReceiver(mWifiBroadcastReceiver); 273 mWifiBroadcastReceiver = null; 274 } 275 } 276 onDeviceFound(@ullable DeviceFilterPair device)277 private void onDeviceFound(@Nullable DeviceFilterPair device) { 278 if (device == null) return; 279 280 Handler.getMain().sendMessage(obtainMessage( 281 CompanionDeviceDiscoveryService::onDeviceFoundMainThread, this, device)); 282 } 283 284 @MainThread onDeviceFoundMainThread(@onNull DeviceFilterPair device)285 void onDeviceFoundMainThread(@NonNull DeviceFilterPair device) { 286 if (mDevicesFound.contains(device)) { 287 Log.i(LOG_TAG, "Skipping device " + device + " - already among found devices"); 288 return; 289 } 290 291 Log.i(LOG_TAG, "Found device " + device); 292 293 if (mDevicesFound.isEmpty()) { 294 onReadyToShowUI(); 295 } 296 mDevicesFound.add(device); 297 CompanionDeviceActivity.notifyDevicesChanged(); 298 } 299 300 //TODO also, on timeout -> call onFailure onReadyToShowUI()301 private void onReadyToShowUI() { 302 try { 303 mFindCallback.onSuccess(PendingIntent.getActivity( 304 this, 0, 305 new Intent(this, CompanionDeviceActivity.class), 306 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT 307 | PendingIntent.FLAG_IMMUTABLE)); 308 } catch (RemoteException e) { 309 throw new RuntimeException(e); 310 } 311 } 312 onDeviceLost(@ullable DeviceFilterPair device)313 private void onDeviceLost(@Nullable DeviceFilterPair device) { 314 Log.i(LOG_TAG, "Lost device " + device.getDisplayName()); 315 Handler.getMain().sendMessage(obtainMessage( 316 CompanionDeviceDiscoveryService::onDeviceLostMainThread, this, device)); 317 } 318 319 @MainThread onDeviceLostMainThread(@ullable DeviceFilterPair device)320 void onDeviceLostMainThread(@Nullable DeviceFilterPair device) { 321 mDevicesFound.remove(device); 322 CompanionDeviceActivity.notifyDevicesChanged(); 323 } 324 onDeviceSelected(String callingPackage, String deviceAddress)325 void onDeviceSelected(String callingPackage, String deviceAddress) { 326 if (callingPackage == null || deviceAddress == null) { 327 return; 328 } 329 mServiceCallback.complete(new Association( 330 getUserId(), deviceAddress, callingPackage, mRequest.getDeviceProfile(), false, 331 System.currentTimeMillis())); 332 } 333 onCancel()334 void onCancel() { 335 if (DEBUG) Log.i(LOG_TAG, "onCancel()"); 336 mActivity = null; 337 mServiceCallback.cancel(true); 338 } 339 340 /** 341 * A pair of device and a filter that matched this device if any. 342 * 343 * @param <T> device type 344 */ 345 static class DeviceFilterPair<T extends Parcelable> { 346 public final T device; 347 @Nullable 348 public final DeviceFilter<T> filter; 349 DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter)350 private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) { 351 this.device = device; 352 this.filter = filter; 353 } 354 355 /** 356 * {@code (device, null)} if the filters list is empty or null 357 * {@code null} if none of the provided filters match the device 358 * {@code (device, filter)} where filter is among the list of filters and matches the device 359 */ 360 @Nullable findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)361 public static <T extends Parcelable> DeviceFilterPair<T> findMatch( 362 T dev, @Nullable List<? extends DeviceFilter<T>> filters) { 363 if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); 364 final DeviceFilter<T> matchingFilter 365 = CollectionUtils.find(filters, f -> f.matches(dev)); 366 367 DeviceFilterPair<T> result = matchingFilter != null 368 ? new DeviceFilterPair<>(dev, matchingFilter) 369 : null; 370 if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters + 371 ") -> " + result); 372 return result; 373 } 374 getDisplayName()375 public String getDisplayName() { 376 if (filter == null) { 377 Preconditions.checkNotNull(device); 378 if (device instanceof BluetoothDevice) { 379 return getDeviceDisplayNameInternal((BluetoothDevice) device); 380 } else if (device instanceof android.net.wifi.ScanResult) { 381 return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device); 382 } else if (device instanceof ScanResult) { 383 return getDeviceDisplayNameInternal(((ScanResult) device).getDevice()); 384 } else { 385 throw new IllegalArgumentException("Unknown device type: " + device.getClass()); 386 } 387 } 388 return filter.getDeviceDisplayName(device); 389 } 390 391 @Override equals(Object o)392 public boolean equals(Object o) { 393 if (this == o) return true; 394 if (o == null || getClass() != o.getClass()) return false; 395 DeviceFilterPair<?> that = (DeviceFilterPair<?>) o; 396 return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device)); 397 } 398 399 @Override hashCode()400 public int hashCode() { 401 return Objects.hash(getDeviceMacAddress(device)); 402 } 403 404 @Override toString()405 public String toString() { 406 return "DeviceFilterPair{" 407 + "device=" + device + " " + getDisplayName() 408 + ", filter=" + filter 409 + '}'; 410 } 411 } 412 413 private class BLEScanCallback extends ScanCallback { 414 BLEScanCallback()415 public BLEScanCallback() { 416 if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this); 417 } 418 419 @Override onScanResult(int callbackType, ScanResult result)420 public void onScanResult(int callbackType, ScanResult result) { 421 if (DEBUG) { 422 Log.i(LOG_TAG, 423 "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result 424 + ")"); 425 } 426 final DeviceFilterPair<ScanResult> deviceFilterPair 427 = DeviceFilterPair.findMatch(result, mBLEFilters); 428 if (deviceFilterPair == null) return; 429 if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { 430 onDeviceLost(deviceFilterPair); 431 } else { 432 onDeviceFound(deviceFilterPair); 433 } 434 } 435 } 436 437 private class BluetoothBroadcastReceiver extends BroadcastReceiver { 438 @Override onReceive(Context context, Intent intent)439 public void onReceive(Context context, Intent intent) { 440 if (DEBUG) { 441 Log.i(LOG_TAG, 442 "BL.onReceive(context = " + context + ", intent = " + intent + ")"); 443 } 444 final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 445 final DeviceFilterPair<BluetoothDevice> deviceFilterPair 446 = DeviceFilterPair.findMatch(device, mBluetoothFilters); 447 if (deviceFilterPair == null) return; 448 if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { 449 onDeviceFound(deviceFilterPair); 450 } else { 451 onDeviceLost(deviceFilterPair); 452 } 453 } 454 } 455 456 private class WifiBroadcastReceiver extends BroadcastReceiver { 457 @Override onReceive(Context context, Intent intent)458 public void onReceive(Context context, Intent intent) { 459 if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { 460 List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults(); 461 462 if (DEBUG) { 463 Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults)); 464 } 465 466 for (int i = 0; i < scanResults.size(); i++) { 467 onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters)); 468 } 469 } 470 } 471 } 472 } 473