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 122 private ScanCallback mBLEScanCallback; 123 private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver; 124 private WifiBroadcastReceiver mWifiBroadcastReceiver; 125 126 @Override onBind(Intent intent)127 public IBinder onBind(Intent intent) { 128 Log.i(LOG_TAG, "onBind(" + intent + ")"); 129 return mBinder.asBinder(); 130 } 131 132 @Override onCreate()133 public void onCreate() { 134 super.onCreate(); 135 136 Log.i(LOG_TAG, "onCreate()"); 137 138 mBluetoothManager = getSystemService(BluetoothManager.class); 139 mBluetoothAdapter = mBluetoothManager.getAdapter(); 140 mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); 141 mWifiManager = getSystemService(WifiManager.class); 142 143 mDevicesFound = new ArrayList<>(); 144 145 sInstance = this; 146 } 147 148 @MainThread startDiscovery(AssociationRequest request)149 private void startDiscovery(AssociationRequest request) { 150 if (!request.equals(mRequest)) { 151 mRequest = request; 152 153 mFilters = request.getDeviceFilters(); 154 mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); 155 mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); 156 mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class); 157 mBLEScanFilters 158 = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter); 159 160 reset(); 161 } else { 162 Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request); 163 } 164 165 if (!ArrayUtils.isEmpty(mDevicesFound)) { 166 onReadyToShowUI(); 167 } 168 169 // If filtering to get single device by mac address, also search in the set of already 170 // bonded devices to allow linking those directly 171 String singleMacAddressFilter = null; 172 if (mRequest.isSingleDevice()) { 173 int numFilters = size(mBluetoothFilters); 174 for (int i = 0; i < numFilters; i++) { 175 BluetoothDeviceFilter filter = mBluetoothFilters.get(i); 176 if (!TextUtils.isEmpty(filter.getAddress())) { 177 singleMacAddressFilter = filter.getAddress(); 178 break; 179 } 180 } 181 } 182 if (singleMacAddressFilter != null) { 183 for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) { 184 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 185 } 186 for (BluetoothDevice dev : emptyIfNull( 187 mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT))) { 188 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 189 } 190 for (BluetoothDevice dev : emptyIfNull( 191 mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER))) { 192 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 193 } 194 } 195 196 if (shouldScan(mBluetoothFilters)) { 197 final IntentFilter intentFilter = new IntentFilter(); 198 intentFilter.addAction(BluetoothDevice.ACTION_FOUND); 199 200 Log.i(LOG_TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)"); 201 mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver(); 202 registerReceiver(mBluetoothBroadcastReceiver, intentFilter); 203 mBluetoothAdapter.startDiscovery(); 204 } 205 206 if (shouldScan(mBLEFilters) && mBLEScanner != null) { 207 Log.i(LOG_TAG, "BLEScanner.startScan"); 208 mBLEScanCallback = new BLEScanCallback(); 209 mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); 210 } 211 212 if (shouldScan(mWifiFilters)) { 213 Log.i(LOG_TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)"); 214 mWifiBroadcastReceiver = new WifiBroadcastReceiver(); 215 registerReceiver(mWifiBroadcastReceiver, 216 new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); 217 mWifiManager.startScan(); 218 } 219 mIsScanning = true; 220 Handler.getMain().sendMessageDelayed( 221 obtainMessage(CompanionDeviceDiscoveryService::stopScan, this), 222 SCAN_TIMEOUT); 223 } 224 shouldScan(List<? extends DeviceFilter> mediumSpecificFilters)225 private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) { 226 return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters); 227 } 228 229 @MainThread reset()230 private void reset() { 231 Log.i(LOG_TAG, "reset()"); 232 stopScan(); 233 mDevicesFound.clear(); 234 mSelectedDevice = null; 235 CompanionDeviceActivity.notifyDevicesChanged(); 236 } 237 238 @Override onUnbind(Intent intent)239 public boolean onUnbind(Intent intent) { 240 Log.i(LOG_TAG, "onUnbind(intent = " + intent + ")"); 241 stopScan(); 242 return super.onUnbind(intent); 243 } 244 stopScan()245 private void stopScan() { 246 Log.i(LOG_TAG, "stopScan()"); 247 248 if (!mIsScanning) return; 249 mIsScanning = false; 250 251 CompanionDeviceActivity activity = mActivity; 252 if (activity != null) { 253 if (activity.mDeviceListView != null) { 254 activity.mDeviceListView.removeFooterView(activity.mLoadingIndicator); 255 } 256 mActivity = null; 257 } 258 259 mBluetoothAdapter.cancelDiscovery(); 260 if (mBluetoothBroadcastReceiver != null) { 261 unregisterReceiver(mBluetoothBroadcastReceiver); 262 mBluetoothBroadcastReceiver = null; 263 } 264 if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback); 265 if (mWifiBroadcastReceiver != null) { 266 unregisterReceiver(mWifiBroadcastReceiver); 267 mWifiBroadcastReceiver = null; 268 } 269 } 270 onDeviceFound(@ullable DeviceFilterPair device)271 private void onDeviceFound(@Nullable DeviceFilterPair device) { 272 if (device == null) return; 273 274 Handler.getMain().sendMessage(obtainMessage( 275 CompanionDeviceDiscoveryService::onDeviceFoundMainThread, this, device)); 276 } 277 278 @MainThread onDeviceFoundMainThread(@onNull DeviceFilterPair device)279 void onDeviceFoundMainThread(@NonNull DeviceFilterPair device) { 280 if (mDevicesFound.contains(device)) { 281 Log.i(LOG_TAG, "Skipping device " + device + " - already among found devices"); 282 return; 283 } 284 285 Log.i(LOG_TAG, "Found device " + device); 286 287 if (mDevicesFound.isEmpty()) { 288 onReadyToShowUI(); 289 } 290 mDevicesFound.add(device); 291 CompanionDeviceActivity.notifyDevicesChanged(); 292 } 293 294 //TODO also, on timeout -> call onFailure onReadyToShowUI()295 private void onReadyToShowUI() { 296 try { 297 mFindCallback.onSuccess(PendingIntent.getActivity( 298 this, 0, 299 new Intent(this, CompanionDeviceActivity.class), 300 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT 301 | PendingIntent.FLAG_IMMUTABLE)); 302 } catch (RemoteException e) { 303 throw new RuntimeException(e); 304 } 305 } 306 onDeviceLost(@ullable DeviceFilterPair device)307 private void onDeviceLost(@Nullable DeviceFilterPair device) { 308 Log.i(LOG_TAG, "Lost device " + device.getDisplayName()); 309 Handler.getMain().sendMessage(obtainMessage( 310 CompanionDeviceDiscoveryService::onDeviceLostMainThread, this, device)); 311 } 312 313 @MainThread onDeviceLostMainThread(@ullable DeviceFilterPair device)314 void onDeviceLostMainThread(@Nullable DeviceFilterPair device) { 315 mDevicesFound.remove(device); 316 CompanionDeviceActivity.notifyDevicesChanged(); 317 } 318 onDeviceSelected(String callingPackage, String deviceAddress)319 void onDeviceSelected(String callingPackage, String deviceAddress) { 320 if (callingPackage == null || deviceAddress == null) { 321 return; 322 } 323 mServiceCallback.complete(new Association( 324 getUserId(), deviceAddress, callingPackage, mRequest.getDeviceProfile(), false, 325 System.currentTimeMillis())); 326 } 327 onCancel()328 void onCancel() { 329 if (DEBUG) Log.i(LOG_TAG, "onCancel()"); 330 mServiceCallback.cancel(true); 331 } 332 333 /** 334 * A pair of device and a filter that matched this device if any. 335 * 336 * @param <T> device type 337 */ 338 static class DeviceFilterPair<T extends Parcelable> { 339 public final T device; 340 @Nullable 341 public final DeviceFilter<T> filter; 342 DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter)343 private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) { 344 this.device = device; 345 this.filter = filter; 346 } 347 348 /** 349 * {@code (device, null)} if the filters list is empty or null 350 * {@code null} if none of the provided filters match the device 351 * {@code (device, filter)} where filter is among the list of filters and matches the device 352 */ 353 @Nullable findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)354 public static <T extends Parcelable> DeviceFilterPair<T> findMatch( 355 T dev, @Nullable List<? extends DeviceFilter<T>> filters) { 356 if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); 357 final DeviceFilter<T> matchingFilter 358 = CollectionUtils.find(filters, f -> f.matches(dev)); 359 360 DeviceFilterPair<T> result = matchingFilter != null 361 ? new DeviceFilterPair<>(dev, matchingFilter) 362 : null; 363 if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters + 364 ") -> " + result); 365 return result; 366 } 367 getDisplayName()368 public String getDisplayName() { 369 if (filter == null) { 370 Preconditions.checkNotNull(device); 371 if (device instanceof BluetoothDevice) { 372 return getDeviceDisplayNameInternal((BluetoothDevice) device); 373 } else if (device instanceof android.net.wifi.ScanResult) { 374 return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device); 375 } else if (device instanceof ScanResult) { 376 return getDeviceDisplayNameInternal(((ScanResult) device).getDevice()); 377 } else { 378 throw new IllegalArgumentException("Unknown device type: " + device.getClass()); 379 } 380 } 381 return filter.getDeviceDisplayName(device); 382 } 383 384 @Override equals(Object o)385 public boolean equals(Object o) { 386 if (this == o) return true; 387 if (o == null || getClass() != o.getClass()) return false; 388 DeviceFilterPair<?> that = (DeviceFilterPair<?>) o; 389 return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device)); 390 } 391 392 @Override hashCode()393 public int hashCode() { 394 return Objects.hash(getDeviceMacAddress(device)); 395 } 396 397 @Override toString()398 public String toString() { 399 return "DeviceFilterPair{" 400 + "device=" + device + " " + getDisplayName() 401 + ", filter=" + filter 402 + '}'; 403 } 404 } 405 406 private class BLEScanCallback extends ScanCallback { 407 BLEScanCallback()408 public BLEScanCallback() { 409 if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this); 410 } 411 412 @Override onScanResult(int callbackType, ScanResult result)413 public void onScanResult(int callbackType, ScanResult result) { 414 if (DEBUG) { 415 Log.i(LOG_TAG, 416 "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result 417 + ")"); 418 } 419 final DeviceFilterPair<ScanResult> deviceFilterPair 420 = DeviceFilterPair.findMatch(result, mBLEFilters); 421 if (deviceFilterPair == null) return; 422 if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { 423 onDeviceLost(deviceFilterPair); 424 } else { 425 onDeviceFound(deviceFilterPair); 426 } 427 } 428 } 429 430 private class BluetoothBroadcastReceiver extends BroadcastReceiver { 431 @Override onReceive(Context context, Intent intent)432 public void onReceive(Context context, Intent intent) { 433 if (DEBUG) { 434 Log.i(LOG_TAG, 435 "BL.onReceive(context = " + context + ", intent = " + intent + ")"); 436 } 437 final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 438 final DeviceFilterPair<BluetoothDevice> deviceFilterPair 439 = DeviceFilterPair.findMatch(device, mBluetoothFilters); 440 if (deviceFilterPair == null) return; 441 if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { 442 onDeviceFound(deviceFilterPair); 443 } else { 444 onDeviceLost(deviceFilterPair); 445 } 446 } 447 } 448 449 private class WifiBroadcastReceiver extends BroadcastReceiver { 450 @Override onReceive(Context context, Intent intent)451 public void onReceive(Context context, Intent intent) { 452 if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { 453 List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults(); 454 455 if (DEBUG) { 456 Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults)); 457 } 458 459 for (int i = 0; i < scanResults.size(); i++) { 460 onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters)); 461 } 462 } 463 } 464 } 465 } 466