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.NonNull; 28 import android.annotation.Nullable; 29 import android.app.PendingIntent; 30 import android.app.Service; 31 import android.bluetooth.BluetoothAdapter; 32 import android.bluetooth.BluetoothDevice; 33 import android.bluetooth.BluetoothManager; 34 import android.bluetooth.le.BluetoothLeScanner; 35 import android.bluetooth.le.ScanCallback; 36 import android.bluetooth.le.ScanFilter; 37 import android.bluetooth.le.ScanResult; 38 import android.bluetooth.le.ScanSettings; 39 import android.companion.AssociationRequest; 40 import android.companion.BluetoothDeviceFilter; 41 import android.companion.BluetoothLeDeviceFilter; 42 import android.companion.DeviceFilter; 43 import android.companion.ICompanionDeviceDiscoveryService; 44 import android.companion.ICompanionDeviceDiscoveryServiceCallback; 45 import android.companion.IFindDeviceCallback; 46 import android.companion.WifiDeviceFilter; 47 import android.content.BroadcastReceiver; 48 import android.content.Context; 49 import android.content.Intent; 50 import android.content.IntentFilter; 51 import android.graphics.Color; 52 import android.graphics.drawable.Drawable; 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 import android.view.View; 61 import android.view.ViewGroup; 62 import android.widget.ArrayAdapter; 63 import android.widget.TextView; 64 65 import com.android.internal.util.ArrayUtils; 66 import com.android.internal.util.CollectionUtils; 67 import com.android.internal.util.Preconditions; 68 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.Objects; 72 73 public class DeviceDiscoveryService extends Service { 74 75 private static final boolean DEBUG = false; 76 private static final String LOG_TAG = "DeviceDiscoveryService"; 77 78 private static final long SCAN_TIMEOUT = 20000; 79 80 static DeviceDiscoveryService sInstance; 81 82 private BluetoothAdapter mBluetoothAdapter; 83 private WifiManager mWifiManager; 84 @Nullable private BluetoothLeScanner mBLEScanner; 85 private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().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 DevicesAdapter mDevicesAdapter; 97 IFindDeviceCallback mFindCallback; 98 99 ICompanionDeviceDiscoveryServiceCallback mServiceCallback; 100 boolean mIsScanning = false; 101 @Nullable DeviceChooserActivity 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 ICompanionDeviceDiscoveryServiceCallback serviceCallback) { 110 if (DEBUG) { 111 Log.i(LOG_TAG, 112 "startDiscovery() called with: filter = [" + request 113 + "], findCallback = [" + findCallback + "]" 114 + "], serviceCallback = [" + serviceCallback + "]"); 115 } 116 mFindCallback = findCallback; 117 mServiceCallback = serviceCallback; 118 DeviceDiscoveryService.this.startDiscovery(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 if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")"); 129 return mBinder.asBinder(); 130 } 131 132 @Override onCreate()133 public void onCreate() { 134 super.onCreate(); 135 136 if (DEBUG) Log.i(LOG_TAG, "onCreate()"); 137 138 mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter(); 139 mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); 140 mWifiManager = getSystemService(WifiManager.class); 141 142 mDevicesFound = new ArrayList<>(); 143 mDevicesAdapter = new DevicesAdapter(); 144 145 sInstance = this; 146 } 147 startDiscovery(AssociationRequest request)148 private void startDiscovery(AssociationRequest request) { 149 if (!request.equals(mRequest)) { 150 mRequest = request; 151 152 mFilters = request.getDeviceFilters(); 153 mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); 154 mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); 155 mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class); 156 mBLEScanFilters = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter); 157 158 reset(); 159 } else if (DEBUG) Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request); 160 161 if (!ArrayUtils.isEmpty(mDevicesFound)) { 162 onReadyToShowUI(); 163 } 164 165 // If filtering to get single device by mac address, also search in the set of already 166 // bonded devices to allow linking those directly 167 String singleMacAddressFilter = null; 168 if (mRequest.isSingleDevice()) { 169 int numFilters = size(mBluetoothFilters); 170 for (int i = 0; i < numFilters; i++) { 171 BluetoothDeviceFilter filter = mBluetoothFilters.get(i); 172 if (!TextUtils.isEmpty(filter.getAddress())) { 173 singleMacAddressFilter = filter.getAddress(); 174 break; 175 } 176 } 177 } 178 if (singleMacAddressFilter != null) { 179 for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) { 180 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); 181 } 182 } 183 184 if (shouldScan(mBluetoothFilters)) { 185 final IntentFilter intentFilter = new IntentFilter(); 186 intentFilter.addAction(BluetoothDevice.ACTION_FOUND); 187 188 mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver(); 189 registerReceiver(mBluetoothBroadcastReceiver, intentFilter); 190 mBluetoothAdapter.startDiscovery(); 191 } 192 193 if (shouldScan(mBLEFilters) && mBLEScanner != null) { 194 mBLEScanCallback = new BLEScanCallback(); 195 mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); 196 } 197 198 if (shouldScan(mWifiFilters)) { 199 mWifiBroadcastReceiver = new WifiBroadcastReceiver(); 200 registerReceiver(mWifiBroadcastReceiver, 201 new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); 202 mWifiManager.startScan(); 203 } 204 mIsScanning = true; 205 Handler.getMain().sendMessageDelayed( 206 obtainMessage(DeviceDiscoveryService::stopScan, this), 207 SCAN_TIMEOUT); 208 } 209 shouldScan(List<? extends DeviceFilter> mediumSpecificFilters)210 private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) { 211 return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters); 212 } 213 reset()214 private void reset() { 215 if (DEBUG) Log.i(LOG_TAG, "reset()"); 216 stopScan(); 217 mDevicesFound.clear(); 218 mSelectedDevice = null; 219 notifyDataSetChanged(); 220 } 221 222 @Override onUnbind(Intent intent)223 public boolean onUnbind(Intent intent) { 224 stopScan(); 225 return super.onUnbind(intent); 226 } 227 stopScan()228 private void stopScan() { 229 if (DEBUG) Log.i(LOG_TAG, "stopScan()"); 230 231 if (!mIsScanning) return; 232 mIsScanning = false; 233 234 DeviceChooserActivity activity = mActivity; 235 if (activity != null) { 236 if (activity.mDeviceListView != null) { 237 activity.mDeviceListView.removeFooterView(activity.mLoadingIndicator); 238 } 239 mActivity = null; 240 } 241 242 mBluetoothAdapter.cancelDiscovery(); 243 if (mBluetoothBroadcastReceiver != null) { 244 unregisterReceiver(mBluetoothBroadcastReceiver); 245 mBluetoothBroadcastReceiver = null; 246 } 247 if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback); 248 if (mWifiBroadcastReceiver != null) { 249 unregisterReceiver(mWifiBroadcastReceiver); 250 mWifiBroadcastReceiver = null; 251 } 252 } 253 onDeviceFound(@ullable DeviceFilterPair device)254 private void onDeviceFound(@Nullable DeviceFilterPair device) { 255 if (device == null) return; 256 257 if (mDevicesFound.contains(device)) { 258 return; 259 } 260 261 if (DEBUG) Log.i(LOG_TAG, "Found device " + device); 262 263 if (mDevicesFound.isEmpty()) { 264 onReadyToShowUI(); 265 } 266 mDevicesFound.add(device); 267 notifyDataSetChanged(); 268 } 269 notifyDataSetChanged()270 private void notifyDataSetChanged() { 271 Handler.getMain().sendMessage(obtainMessage( 272 DevicesAdapter::notifyDataSetChanged, mDevicesAdapter)); 273 } 274 275 //TODO also, on timeout -> call onFailure onReadyToShowUI()276 private void onReadyToShowUI() { 277 try { 278 mFindCallback.onSuccess(PendingIntent.getActivity( 279 this, 0, 280 new Intent(this, DeviceChooserActivity.class), 281 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT 282 | PendingIntent.FLAG_IMMUTABLE)); 283 } catch (RemoteException e) { 284 throw new RuntimeException(e); 285 } 286 } 287 onDeviceLost(@ullable DeviceFilterPair device)288 private void onDeviceLost(@Nullable DeviceFilterPair device) { 289 mDevicesFound.remove(device); 290 notifyDataSetChanged(); 291 if (DEBUG) Log.i(LOG_TAG, "Lost device " + device.getDisplayName()); 292 } 293 onDeviceSelected(String callingPackage, String deviceAddress)294 void onDeviceSelected(String callingPackage, String deviceAddress) { 295 try { 296 mServiceCallback.onDeviceSelected( 297 //TODO is this the right userId? 298 callingPackage, getUserId(), deviceAddress); 299 } catch (RemoteException e) { 300 Log.e(LOG_TAG, "Failed to record association: " 301 + callingPackage + " <-> " + deviceAddress); 302 } 303 } 304 onCancel()305 void onCancel() { 306 if (DEBUG) Log.i(LOG_TAG, "onCancel()"); 307 try { 308 mServiceCallback.onDeviceSelectionCancel(); 309 } catch (RemoteException e) { 310 throw new RuntimeException(e); 311 } 312 } 313 314 class DevicesAdapter extends ArrayAdapter<DeviceFilterPair> { 315 private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth); 316 private Drawable WIFI_ICON = icon(com.android.internal.R.drawable.ic_wifi_signal_3); 317 icon(int drawableRes)318 private Drawable icon(int drawableRes) { 319 Drawable icon = getResources().getDrawable(drawableRes, null); 320 icon.setTint(Color.DKGRAY); 321 return icon; 322 } 323 DevicesAdapter()324 public DevicesAdapter() { 325 super(DeviceDiscoveryService.this, 0, mDevicesFound); 326 } 327 328 @Override getView( int position, @Nullable View convertView, @NonNull ViewGroup parent)329 public View getView( 330 int position, 331 @Nullable View convertView, 332 @NonNull ViewGroup parent) { 333 TextView view = convertView instanceof TextView 334 ? (TextView) convertView 335 : newView(); 336 bind(view, getItem(position)); 337 return view; 338 } 339 bind(TextView textView, DeviceFilterPair device)340 private void bind(TextView textView, DeviceFilterPair device) { 341 textView.setText(device.getDisplayName()); 342 textView.setBackgroundColor( 343 device.equals(mSelectedDevice) 344 ? Color.GRAY 345 : Color.TRANSPARENT); 346 textView.setCompoundDrawablesWithIntrinsicBounds( 347 device.device instanceof android.net.wifi.ScanResult 348 ? WIFI_ICON 349 : BLUETOOTH_ICON, 350 null, null, null); 351 textView.setOnClickListener((view) -> { 352 mSelectedDevice = device; 353 notifyDataSetChanged(); 354 }); 355 } 356 357 //TODO move to a layout file newView()358 private TextView newView() { 359 final TextView textView = new TextView(DeviceDiscoveryService.this); 360 textView.setTextColor(Color.BLACK); 361 final int padding = DeviceChooserActivity.getPadding(getResources()); 362 textView.setPadding(padding, padding, padding, padding); 363 textView.setCompoundDrawablePadding(padding); 364 return textView; 365 } 366 } 367 368 /** 369 * A pair of device and a filter that matched this device if any. 370 * 371 * @param <T> device type 372 */ 373 static class DeviceFilterPair<T extends Parcelable> { 374 public final T device; 375 @Nullable 376 public final DeviceFilter<T> filter; 377 DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter)378 private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) { 379 this.device = device; 380 this.filter = filter; 381 } 382 383 /** 384 * {@code (device, null)} if the filters list is empty or null 385 * {@code null} if none of the provided filters match the device 386 * {@code (device, filter)} where filter is among the list of filters and matches the device 387 */ 388 @Nullable findMatch( T dev, @Nullable List<? extends DeviceFilter<T>> filters)389 public static <T extends Parcelable> DeviceFilterPair<T> findMatch( 390 T dev, @Nullable List<? extends DeviceFilter<T>> filters) { 391 if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); 392 final DeviceFilter<T> matchingFilter 393 = CollectionUtils.find(filters, f -> f.matches(dev)); 394 395 DeviceFilterPair<T> result = matchingFilter != null 396 ? new DeviceFilterPair<>(dev, matchingFilter) 397 : null; 398 if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters + 399 ") -> " + result); 400 return result; 401 } 402 getDisplayName()403 public String getDisplayName() { 404 if (filter == null) { 405 Preconditions.checkNotNull(device); 406 if (device instanceof BluetoothDevice) { 407 return getDeviceDisplayNameInternal((BluetoothDevice) device); 408 } else if (device instanceof android.net.wifi.ScanResult) { 409 return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device); 410 } else if (device instanceof ScanResult) { 411 return getDeviceDisplayNameInternal(((ScanResult) device).getDevice()); 412 } else { 413 throw new IllegalArgumentException("Unknown device type: " + device.getClass()); 414 } 415 } 416 return filter.getDeviceDisplayName(device); 417 } 418 419 @Override equals(Object o)420 public boolean equals(Object o) { 421 if (this == o) return true; 422 if (o == null || getClass() != o.getClass()) return false; 423 DeviceFilterPair<?> that = (DeviceFilterPair<?>) o; 424 return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device)); 425 } 426 427 @Override hashCode()428 public int hashCode() { 429 return Objects.hash(getDeviceMacAddress(device)); 430 } 431 432 @Override toString()433 public String toString() { 434 return "DeviceFilterPair{" + 435 "device=" + device + 436 ", filter=" + filter + 437 '}'; 438 } 439 } 440 441 private class BLEScanCallback extends ScanCallback { 442 BLEScanCallback()443 public BLEScanCallback() { 444 if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this); 445 } 446 447 @Override onScanResult(int callbackType, ScanResult result)448 public void onScanResult(int callbackType, ScanResult result) { 449 if (DEBUG) { 450 Log.i(LOG_TAG, 451 "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result 452 + ")"); 453 } 454 final DeviceFilterPair<ScanResult> deviceFilterPair 455 = DeviceFilterPair.findMatch(result, mBLEFilters); 456 if (deviceFilterPair == null) return; 457 if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { 458 onDeviceLost(deviceFilterPair); 459 } else { 460 onDeviceFound(deviceFilterPair); 461 } 462 } 463 } 464 465 private class BluetoothBroadcastReceiver extends BroadcastReceiver { 466 @Override onReceive(Context context, Intent intent)467 public void onReceive(Context context, Intent intent) { 468 if (DEBUG) { 469 Log.i(LOG_TAG, 470 "BL.onReceive(context = " + context + ", intent = " + intent + ")"); 471 } 472 final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 473 final DeviceFilterPair<BluetoothDevice> deviceFilterPair 474 = DeviceFilterPair.findMatch(device, mBluetoothFilters); 475 if (deviceFilterPair == null) return; 476 if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { 477 onDeviceFound(deviceFilterPair); 478 } else { 479 onDeviceLost(deviceFilterPair); 480 } 481 } 482 } 483 484 private class WifiBroadcastReceiver extends BroadcastReceiver { 485 @Override onReceive(Context context, Intent intent)486 public void onReceive(Context context, Intent intent) { 487 if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { 488 List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults(); 489 490 if (DEBUG) { 491 Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults)); 492 } 493 494 for (int i = 0; i < scanResults.size(); i++) { 495 onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters)); 496 } 497 } 498 } 499 } 500 } 501