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