1 /* 2 * Copyright (C) 2017 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 android.companion; 18 19 import android.Manifest; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SystemApi; 24 import android.annotation.SystemService; 25 import android.app.Activity; 26 import android.app.Application; 27 import android.app.PendingIntent; 28 import android.bluetooth.BluetoothDevice; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.IntentSender; 32 import android.content.pm.PackageManager; 33 import android.net.MacAddress; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.RemoteException; 37 import android.os.UserHandle; 38 import android.service.notification.NotificationListenerService; 39 import android.util.ExceptionUtils; 40 import android.util.Log; 41 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Objects; 45 import java.util.function.BiConsumer; 46 47 /** 48 * System level service for managing companion devices 49 * 50 * See <a href="{@docRoot}guide/topics/connectivity/companion-device-pairing">this guide</a> 51 * for a usage example. 52 * 53 * <p>To obtain an instance call {@link Context#getSystemService}({@link 54 * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest, 55 * Callback, Handler)} to initiate the flow of associating current package with a 56 * device selected by user.</p> 57 * 58 * @see CompanionDeviceManager#associate 59 * @see AssociationRequest 60 */ 61 @SystemService(Context.COMPANION_DEVICE_SERVICE) 62 public final class CompanionDeviceManager { 63 64 private static final boolean DEBUG = false; 65 private static final String LOG_TAG = "CompanionDeviceManager"; 66 67 /** 68 * A device, returned in the activity result of the {@link IntentSender} received in 69 * {@link Callback#onDeviceFound} 70 * 71 * Type is: 72 * <ul> 73 * <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li> 74 * <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li> 75 * <li>for WiFi - {@link android.net.wifi.ScanResult}</li> 76 * </ul> 77 */ 78 public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE"; 79 80 /** 81 * The package name of the companion device discovery component. 82 * 83 * @hide 84 */ 85 public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME = 86 "com.android.companiondevicemanager"; 87 88 /** 89 * A callback to receive once at least one suitable device is found, or the search failed 90 * (e.g. timed out) 91 */ 92 public abstract static class Callback { 93 94 /** 95 * Called once at least one suitable device is found 96 * 97 * @param chooserLauncher a {@link IntentSender} to launch the UI for user to select a 98 * device 99 */ onDeviceFound(IntentSender chooserLauncher)100 public abstract void onDeviceFound(IntentSender chooserLauncher); 101 102 /** 103 * Called if there was an error looking for device(s) 104 * 105 * @param error the cause of the error 106 */ onFailure(CharSequence error)107 public abstract void onFailure(CharSequence error); 108 } 109 110 private final ICompanionDeviceManager mService; 111 private final Context mContext; 112 113 /** @hide */ CompanionDeviceManager( @ullable ICompanionDeviceManager service, @NonNull Context context)114 public CompanionDeviceManager( 115 @Nullable ICompanionDeviceManager service, @NonNull Context context) { 116 mService = service; 117 mContext = context; 118 } 119 120 /** 121 * Associate this app with a companion device, selected by user 122 * 123 * <p>Once at least one appropriate device is found, {@code callback} will be called with a 124 * {@link PendingIntent} that can be used to show the list of available devices for the user 125 * to select. 126 * It should be started for result (i.e. using 127 * {@link android.app.Activity#startIntentSenderForResult}), as the resulting 128 * {@link android.content.Intent} will contain extra {@link #EXTRA_DEVICE}, with the selected 129 * device. (e.g. {@link android.bluetooth.BluetoothDevice})</p> 130 * 131 * <p>If your app needs to be excluded from battery optimizations (run in the background) 132 * or to have unrestricted data access (use data in the background) you can declare that 133 * you use the {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and {@link 134 * android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} respectively. Note that these 135 * special capabilities have a negative effect on the device's battery and user's data 136 * usage, therefore you should request them when absolutely necessary.</p> 137 * 138 * <p>You can call {@link #getAssociations} to get the list of currently associated 139 * devices, and {@link #disassociate} to remove an association. Consider doing so when the 140 * association is no longer relevant to avoid unnecessary battery and/or data drain resulting 141 * from special privileges that the association provides</p> 142 * 143 * <p>Calling this API requires a uses-feature 144 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 145 * 146 * <p>When using {@link AssociationRequest#DEVICE_PROFILE_WATCH watch} 147 * {@link AssociationRequest.Builder#setDeviceProfile profile}, caller must also hold 148 * {@link Manifest.permission#REQUEST_COMPANION_PROFILE_WATCH}</p> 149 * 150 * @param request specific details about this request 151 * @param callback will be called once there's at least one device found for user to choose from 152 * @param handler A handler to control which thread the callback will be delivered on, or null, 153 * to deliver it on main thread 154 * 155 * @see AssociationRequest 156 */ 157 @RequiresPermission( 158 value = Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH, 159 conditional = true) associate( @onNull AssociationRequest request, @NonNull Callback callback, @Nullable Handler handler)160 public void associate( 161 @NonNull AssociationRequest request, 162 @NonNull Callback callback, 163 @Nullable Handler handler) { 164 if (!checkFeaturePresent()) { 165 return; 166 } 167 Objects.requireNonNull(request, "Request cannot be null"); 168 Objects.requireNonNull(callback, "Callback cannot be null"); 169 try { 170 mService.associate( 171 request, 172 new CallbackProxy(request, callback, Handler.mainIfNull(handler)), 173 getCallingPackage()); 174 } catch (RemoteException e) { 175 throw e.rethrowFromSystemServer(); 176 } 177 } 178 179 /** 180 * <p>Calling this API requires a uses-feature 181 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 182 * 183 * @return a list of MAC addresses of devices that have been previously associated with the 184 * current app. You can use these with {@link #disassociate} 185 */ 186 @NonNull getAssociations()187 public List<String> getAssociations() { 188 if (!checkFeaturePresent()) { 189 return Collections.emptyList(); 190 } 191 try { 192 return mService.getAssociations(getCallingPackage(), mContext.getUserId()); 193 } catch (RemoteException e) { 194 throw e.rethrowFromSystemServer(); 195 } 196 } 197 198 /** 199 * Remove the association between this app and the device with the given mac address. 200 * 201 * <p>Any privileges provided via being associated with a given device will be revoked</p> 202 * 203 * <p>Consider doing so when the 204 * association is no longer relevant to avoid unnecessary battery and/or data drain resulting 205 * from special privileges that the association provides</p> 206 * 207 * <p>Calling this API requires a uses-feature 208 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 209 * 210 * @param deviceMacAddress the MAC address of device to disassociate from this app 211 */ disassociate(@onNull String deviceMacAddress)212 public void disassociate(@NonNull String deviceMacAddress) { 213 if (!checkFeaturePresent()) { 214 return; 215 } 216 try { 217 mService.disassociate(deviceMacAddress, getCallingPackage()); 218 } catch (RemoteException e) { 219 throw e.rethrowFromSystemServer(); 220 } 221 } 222 223 /** 224 * Request notification access for the given component. 225 * 226 * The given component must follow the protocol specified in {@link NotificationListenerService} 227 * 228 * Only components from the same {@link ComponentName#getPackageName package} as the calling app 229 * are allowed. 230 * 231 * Your app must have an association with a device before calling this API 232 * 233 * <p>Calling this API requires a uses-feature 234 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 235 */ requestNotificationAccess(ComponentName component)236 public void requestNotificationAccess(ComponentName component) { 237 if (!checkFeaturePresent()) { 238 return; 239 } 240 try { 241 IntentSender intentSender = mService.requestNotificationAccess(component) 242 .getIntentSender(); 243 mContext.startIntentSender(intentSender, null, 0, 0, 0); 244 } catch (RemoteException e) { 245 throw e.rethrowFromSystemServer(); 246 } catch (IntentSender.SendIntentException e) { 247 throw new RuntimeException(e); 248 } 249 } 250 251 /** 252 * Check whether the given component can access the notifications via a 253 * {@link NotificationListenerService} 254 * 255 * Your app must have an association with a device before calling this API 256 * 257 * <p>Calling this API requires a uses-feature 258 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 259 * 260 * @param component the name of the component 261 * @return whether the given component has the notification listener permission 262 */ hasNotificationAccess(ComponentName component)263 public boolean hasNotificationAccess(ComponentName component) { 264 if (!checkFeaturePresent()) { 265 return false; 266 } 267 try { 268 return mService.hasNotificationAccess(component); 269 } catch (RemoteException e) { 270 throw e.rethrowFromSystemServer(); 271 } 272 } 273 274 /** 275 * Check if a given package was {@link #associate associated} with a device with given 276 * Wi-Fi MAC address for a given user. 277 * 278 * <p>This is a system API protected by the 279 * {@link andrioid.Manifest.permission#MANAGE_COMPANION_DEVICES} permission, that’s currently 280 * called by the Android Wi-Fi stack to determine whether user consent is required to connect 281 * to a Wi-Fi network. Devices that have been pre-registered as companion devices will not 282 * require user consent to connect.</p> 283 * 284 * <p>Note if the caller has the 285 * {@link android.Manifest.permission#COMPANION_APPROVE_WIFI_CONNECTIONS} permission, this 286 * method will return true by default.</p> 287 * 288 * @param packageName the name of the package that has the association with the companion device 289 * @param macAddress the Wi-Fi MAC address or BSSID of the companion device to check for 290 * @param user the user handle that currently hosts the package being queried for a companion 291 * device association 292 * @return whether a corresponding association record exists 293 * 294 * @hide 295 */ 296 @SystemApi 297 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) isDeviceAssociatedForWifiConnection( @onNull String packageName, @NonNull MacAddress macAddress, @NonNull UserHandle user)298 public boolean isDeviceAssociatedForWifiConnection( 299 @NonNull String packageName, 300 @NonNull MacAddress macAddress, 301 @NonNull UserHandle user) { 302 if (!checkFeaturePresent()) { 303 return false; 304 } 305 Objects.requireNonNull(packageName, "package name cannot be null"); 306 Objects.requireNonNull(macAddress, "mac address cannot be null"); 307 Objects.requireNonNull(user, "user cannot be null"); 308 try { 309 return mService.isDeviceAssociatedForWifiConnection( 310 packageName, macAddress.toString(), user.getIdentifier()); 311 } catch (RemoteException e) { 312 throw e.rethrowFromSystemServer(); 313 } 314 } 315 316 /** 317 * Gets all package-device {@link Association}s for the current user. 318 * 319 * @return the associations list 320 * @hide 321 */ 322 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) getAllAssociations()323 public @NonNull List<Association> getAllAssociations() { 324 if (!checkFeaturePresent()) { 325 return Collections.emptyList(); 326 } 327 try { 328 return mService.getAssociationsForUser(mContext.getUser().getIdentifier()); 329 } catch (RemoteException e) { 330 throw e.rethrowFromSystemServer(); 331 } 332 } 333 334 /** 335 * Checks whether the bluetooth device represented by the mac address was recently associated 336 * with the companion app. This allows these devices to skip the Bluetooth pairing dialog if 337 * their pairing variant is {@link BluetoothDevice#PAIRING_VARIANT_CONSENT}. 338 * 339 * @param packageName the package name of the calling app 340 * @param deviceMacAddress the bluetooth device's mac address 341 * @param user the user handle that currently hosts the package being queried for a companion 342 * device association 343 * @return true if it was recently associated and we can bypass the dialog, false otherwise 344 * @hide 345 */ 346 @SystemApi 347 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) canPairWithoutPrompt(@onNull String packageName, @NonNull String deviceMacAddress, @NonNull UserHandle user)348 public boolean canPairWithoutPrompt(@NonNull String packageName, 349 @NonNull String deviceMacAddress, @NonNull UserHandle user) { 350 if (!checkFeaturePresent()) { 351 return false; 352 } 353 Objects.requireNonNull(packageName, "package name cannot be null"); 354 Objects.requireNonNull(deviceMacAddress, "device mac address cannot be null"); 355 Objects.requireNonNull(user, "user handle cannot be null"); 356 try { 357 return mService.canPairWithoutPrompt(packageName, deviceMacAddress, 358 user.getIdentifier()); 359 } catch (RemoteException e) { 360 throw e.rethrowFromSystemServer(); 361 } 362 } 363 364 /** 365 * Register to receive callbacks whenever the associated device comes in and out of range. 366 * 367 * The provided device must be {@link #associate associated} with the calling app before 368 * calling this method. 369 * 370 * Caller must implement a single {@link CompanionDeviceService} which will be bound to and 371 * receive callbacks to {@link CompanionDeviceService#onDeviceAppeared} and 372 * {@link CompanionDeviceService#onDeviceDisappeared}. 373 * The app doesn't need to remain running in order to receive its callbacks. 374 * 375 * Calling app must declare uses-permission 376 * {@link android.Manifest.permission#REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE}. 377 * 378 * Calling app must check for feature presence of 379 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API. 380 * 381 * For Bluetooth LE devices this is based on scanning for device with the given address. 382 * For Bluetooth classic devices this is triggered when the device connects/disconnects. 383 * WiFi devices are not supported. 384 * 385 * If a Bluetooth LE device wants to use a rotating mac address, it is recommended to use 386 * Resolvable Private Address, and ensure the device is bonded to the phone so that android OS 387 * is able to resolve the address. 388 * 389 * @param deviceAddress a previously-associated companion device's address 390 * 391 * @throws DeviceNotAssociatedException if the given device was not previously associated 392 * with this app. 393 */ 394 @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) startObservingDevicePresence(@onNull String deviceAddress)395 public void startObservingDevicePresence(@NonNull String deviceAddress) 396 throws DeviceNotAssociatedException { 397 if (!checkFeaturePresent()) { 398 return; 399 } 400 Objects.requireNonNull(deviceAddress, "address cannot be null"); 401 try { 402 mService.registerDevicePresenceListenerService( 403 mContext.getPackageName(), deviceAddress); 404 } catch (RemoteException e) { 405 ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); 406 throw e.rethrowFromSystemServer(); 407 } 408 } 409 410 /** 411 * Unregister for receiving callbacks whenever the associated device comes in and out of range. 412 * 413 * The provided device must be {@link #associate associated} with the calling app before 414 * calling this method. 415 * 416 * Calling app must declare uses-permission 417 * {@link android.Manifest.permission#REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE}. 418 * 419 * Calling app must check for feature presence of 420 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API. 421 * 422 * @param deviceAddress a previously-associated companion device's address 423 * 424 * @throws DeviceNotAssociatedException if the given device was not previously associated 425 * with this app. 426 */ 427 @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) stopObservingDevicePresence(@onNull String deviceAddress)428 public void stopObservingDevicePresence(@NonNull String deviceAddress) 429 throws DeviceNotAssociatedException { 430 if (!checkFeaturePresent()) { 431 return; 432 } 433 Objects.requireNonNull(deviceAddress, "address cannot be null"); 434 try { 435 mService.unregisterDevicePresenceListenerService( 436 mContext.getPackageName(), deviceAddress); 437 } catch (RemoteException e) { 438 ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); 439 } 440 } 441 442 /** 443 * Associates given device with given app for the given user directly, without UI prompt. 444 * 445 * @param packageName package name of the companion app 446 * @param macAddress mac address of the device to associate 447 * @param certificate The SHA256 digest of the companion app's signing certificate 448 * 449 * @hide 450 */ 451 @SystemApi 452 @RequiresPermission(android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES) associate( @onNull String packageName, @NonNull MacAddress macAddress, @NonNull byte[] certificate)453 public void associate( 454 @NonNull String packageName, 455 @NonNull MacAddress macAddress, 456 @NonNull byte[] certificate) { 457 if (!checkFeaturePresent()) { 458 return; 459 } 460 Objects.requireNonNull(packageName, "package name cannot be null"); 461 Objects.requireNonNull(macAddress, "mac address cannot be null"); 462 463 UserHandle user = android.os.Process.myUserHandle(); 464 try { 465 mService.createAssociation( 466 packageName, macAddress.toString(), user.getIdentifier(), certificate); 467 } catch (RemoteException e) { 468 throw e.rethrowFromSystemServer(); 469 } 470 } 471 checkFeaturePresent()472 private boolean checkFeaturePresent() { 473 boolean featurePresent = mService != null; 474 if (!featurePresent && DEBUG) { 475 Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP 476 + " not available"); 477 } 478 return featurePresent; 479 } 480 getActivity()481 private Activity getActivity() { 482 return (Activity) mContext; 483 } 484 getCallingPackage()485 private String getCallingPackage() { 486 return mContext.getPackageName(); 487 } 488 489 private class CallbackProxy extends IFindDeviceCallback.Stub 490 implements Application.ActivityLifecycleCallbacks { 491 492 private Callback mCallback; 493 private Handler mHandler; 494 private AssociationRequest mRequest; 495 496 final Object mLock = new Object(); 497 CallbackProxy(AssociationRequest request, Callback callback, Handler handler)498 private CallbackProxy(AssociationRequest request, Callback callback, Handler handler) { 499 mCallback = callback; 500 mHandler = handler; 501 mRequest = request; 502 getActivity().getApplication().registerActivityLifecycleCallbacks(this); 503 } 504 505 @Override onSuccess(PendingIntent launcher)506 public void onSuccess(PendingIntent launcher) { 507 lockAndPost(Callback::onDeviceFound, launcher.getIntentSender()); 508 } 509 510 @Override onFailure(CharSequence reason)511 public void onFailure(CharSequence reason) { 512 lockAndPost(Callback::onFailure, reason); 513 } 514 lockAndPost(BiConsumer<Callback, T> action, T payload)515 <T> void lockAndPost(BiConsumer<Callback, T> action, T payload) { 516 synchronized (mLock) { 517 if (mHandler != null) { 518 mHandler.post(() -> { 519 Callback callback = null; 520 synchronized (mLock) { 521 callback = mCallback; 522 } 523 if (callback != null) { 524 action.accept(callback, payload); 525 } 526 }); 527 } 528 } 529 } 530 531 @Override onActivityDestroyed(Activity activity)532 public void onActivityDestroyed(Activity activity) { 533 synchronized (mLock) { 534 if (activity != getActivity()) return; 535 try { 536 mService.stopScan(mRequest, this, getCallingPackage()); 537 } catch (RemoteException e) { 538 e.rethrowFromSystemServer(); 539 } 540 getActivity().getApplication().unregisterActivityLifecycleCallbacks(this); 541 mCallback = null; 542 mHandler = null; 543 mRequest = null; 544 } 545 } 546 onActivityCreated(Activity activity, Bundle savedInstanceState)547 @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} onActivityStarted(Activity activity)548 @Override public void onActivityStarted(Activity activity) {} onActivityResumed(Activity activity)549 @Override public void onActivityResumed(Activity activity) {} onActivityPaused(Activity activity)550 @Override public void onActivityPaused(Activity activity) {} onActivityStopped(Activity activity)551 @Override public void onActivityStopped(Activity activity) {} onActivitySaveInstanceState(Activity activity, Bundle outState)552 @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} 553 } 554 } 555