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 static android.Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING; 20 import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION; 21 import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER; 22 import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.annotation.RequiresPermission; 27 import android.annotation.SystemApi; 28 import android.annotation.SystemService; 29 import android.annotation.UserHandleAware; 30 import android.app.Activity; 31 import android.app.NotificationManager; 32 import android.app.PendingIntent; 33 import android.bluetooth.BluetoothDevice; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.content.IntentSender; 38 import android.content.pm.PackageManager; 39 import android.net.MacAddress; 40 import android.os.Handler; 41 import android.os.RemoteException; 42 import android.os.UserHandle; 43 import android.service.notification.NotificationListenerService; 44 import android.util.ExceptionUtils; 45 import android.util.Log; 46 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.internal.util.CollectionUtils; 49 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.Iterator; 53 import java.util.List; 54 import java.util.Objects; 55 import java.util.concurrent.Executor; 56 import java.util.function.Consumer; 57 58 /** 59 * System level service for managing companion devices 60 * 61 * See <a href="{@docRoot}guide/topics/connectivity/companion-device-pairing">this guide</a> 62 * for a usage example. 63 * 64 * <p>To obtain an instance call {@link Context#getSystemService}({@link 65 * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest, 66 * Callback, Handler)} to initiate the flow of associating current package with a 67 * device selected by user.</p> 68 * 69 * @see CompanionDeviceManager#associate 70 * @see AssociationRequest 71 */ 72 @SystemService(Context.COMPANION_DEVICE_SERVICE) 73 public final class CompanionDeviceManager { 74 75 private static final boolean DEBUG = false; 76 private static final String LOG_TAG = "CompanionDeviceManager"; 77 78 /** 79 * The result code to propagate back to the originating activity, indicates the association 80 * dialog is explicitly declined by the users. 81 * 82 * @hide 83 */ 84 public static final int RESULT_USER_REJECTED = 1; 85 86 /** 87 * The result code to propagate back to the originating activity, indicates the association 88 * dialog is dismissed if there's no device found after 20 seconds. 89 * 90 * @hide 91 */ 92 public static final int RESULT_DISCOVERY_TIMEOUT = 2; 93 94 /** 95 * The result code to propagate back to the originating activity, indicates the internal error 96 * in CompanionDeviceManager. 97 * 98 * @hide 99 */ 100 public static final int RESULT_INTERNAL_ERROR = 3; 101 102 /** 103 * Requesting applications will receive the String in {@link Callback#onFailure} if the 104 * association dialog is explicitly declined by the users. e.g. press the Don't allow button. 105 * 106 * @hide 107 */ 108 public static final String REASON_USER_REJECTED = "user_rejected"; 109 110 /** 111 * Requesting applications will receive the String in {@link Callback#onFailure} if there's 112 * no device found after 20 seconds. 113 * 114 * @hide 115 */ 116 public static final String REASON_DISCOVERY_TIMEOUT = "discovery_timeout"; 117 118 /** 119 * Requesting applications will receive the String in {@link Callback#onFailure} if the 120 * association dialog is in-explicitly declined by the users. e.g. phone is locked, switch to 121 * another app or press outside the dialog. 122 * 123 * @hide 124 */ 125 public static final String REASON_CANCELED = "canceled"; 126 127 128 /** 129 * A device, returned in the activity result of the {@link IntentSender} received in 130 * {@link Callback#onDeviceFound} 131 * 132 * Type is: 133 * <ul> 134 * <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li> 135 * <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li> 136 * <li>for WiFi - {@link android.net.wifi.ScanResult}</li> 137 * </ul> 138 * 139 * @deprecated use {@link #EXTRA_ASSOCIATION} instead. 140 */ 141 @Deprecated 142 public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE"; 143 144 /** 145 * Extra field name for the {@link AssociationInfo} object, included into 146 * {@link android.content.Intent} which application receive in 147 * {@link Activity#onActivityResult(int, int, Intent)} after the application's 148 * {@link AssociationRequest} was successfully processed and an association was created. 149 */ 150 public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION"; 151 152 /** 153 * The package name of the companion device discovery component. 154 * 155 * @hide 156 */ 157 public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME = 158 "com.android.companiondevicemanager"; 159 160 /** 161 * Callback for applications to receive updates about and the outcome of 162 * {@link AssociationRequest} issued via {@code associate()} call. 163 * 164 * <p> 165 * The {@link Callback#onAssociationPending(IntentSender)} is invoked after the 166 * {@link AssociationRequest} has been checked by the Companion Device Manager Service and is 167 * pending user's approval. 168 * 169 * The {@link IntentSender} received as an argument to 170 * {@link Callback#onAssociationPending(IntentSender)} "encapsulates" an {@link Activity} 171 * that has UI for the user to: 172 * <ul> 173 * <li> 174 * choose the device to associate the application with (if multiple eligible devices are 175 * available) 176 * </li> 177 * <li>confirm the association</li> 178 * <li> 179 * approve the privileges the application will be granted if the association is to be created 180 * </li> 181 * </ul> 182 * 183 * If the Companion Device Manager Service needs to scan for the devices, the {@link Activity} 184 * will also display the status and the progress of the scan. 185 * 186 * Note that Companion Device Manager Service will only start the scanning after the 187 * {@link Activity} was launched and became visible. 188 * 189 * Applications are expected to launch the UI using the received {@link IntentSender} via 190 * {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}. 191 * </p> 192 * 193 * <p> 194 * Upon receiving user's confirmation Companion Device Manager Service will create an 195 * association and will send an {@link AssociationInfo} object that represents the created 196 * association back to the application both via 197 * {@link Callback#onAssociationCreated(AssociationInfo)} and 198 * via {@link Activity#setResult(int, Intent)}. 199 * In the latter the {@code resultCode} will be set to {@link Activity#RESULT_OK} and the 200 * {@code data} {@link Intent} will contain {@link AssociationInfo} extra named 201 * {@link #EXTRA_ASSOCIATION}. 202 * <pre> 203 * <code> 204 * if (resultCode == Activity.RESULT_OK) { 205 * AssociationInfo associationInfo = data.getParcelableExtra(EXTRA_ASSOCIATION); 206 * } 207 * </code> 208 * </pre> 209 * </p> 210 * 211 * <p> 212 * If the Companion Device Manager Service is not able to create an association, it will 213 * invoke {@link Callback#onFailure(CharSequence)}. 214 * 215 * If this happened after the application has launched the UI (eg. the user chose to reject 216 * the association), the outcome will also be delivered to the applications via 217 * {@link Activity#setResult(int)} with the {@link Activity#RESULT_CANCELED} 218 * {@code resultCode}. 219 * </p> 220 * 221 * <p> 222 * Note that in some cases the Companion Device Manager Service may not need to collect 223 * user's approval for creating an association. In such cases, this method will not be 224 * invoked, and {@link #onAssociationCreated(AssociationInfo)} may be invoked right away. 225 * </p> 226 * 227 * @see #associate(AssociationRequest, Executor, Callback) 228 * @see #associate(AssociationRequest, Callback, Handler) 229 * @see #EXTRA_ASSOCIATION 230 */ 231 public abstract static class Callback { 232 /** 233 * @deprecated method was renamed to onAssociationPending() to provide better clarity; both 234 * methods are functionally equivalent and only one needs to be overridden. 235 * 236 * @see #onAssociationPending(IntentSender) 237 */ 238 @Deprecated onDeviceFound(@onNull IntentSender intentSender)239 public void onDeviceFound(@NonNull IntentSender intentSender) {} 240 241 /** 242 * Invoked when the association needs to approved by the user. 243 * 244 * Applications should launch the {@link Activity} "encapsulated" in {@code intentSender} 245 * {@link IntentSender} object by calling 246 * {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}. 247 * 248 * @param intentSender an {@link IntentSender} which applications should use to launch 249 * the UI for the user to confirm the association. 250 */ onAssociationPending(@onNull IntentSender intentSender)251 public void onAssociationPending(@NonNull IntentSender intentSender) { 252 onDeviceFound(intentSender); 253 } 254 255 /** 256 * Invoked when the association is created. 257 * 258 * @param associationInfo contains details of the newly-established association. 259 */ onAssociationCreated(@onNull AssociationInfo associationInfo)260 public void onAssociationCreated(@NonNull AssociationInfo associationInfo) {} 261 262 /** 263 * Invoked if the association could not be created. 264 * 265 * @param error error message. 266 */ onFailure(@ullable CharSequence error)267 public abstract void onFailure(@Nullable CharSequence error); 268 } 269 270 private final ICompanionDeviceManager mService; 271 private Context mContext; 272 273 @GuardedBy("mListeners") 274 private final ArrayList<OnAssociationsChangedListenerProxy> mListeners = new ArrayList<>(); 275 276 /** @hide */ CompanionDeviceManager( @ullable ICompanionDeviceManager service, @NonNull Context context)277 public CompanionDeviceManager( 278 @Nullable ICompanionDeviceManager service, @NonNull Context context) { 279 mService = service; 280 mContext = context; 281 } 282 283 /** 284 * Request to associate this app with a companion device. 285 * 286 * <p>Note that before creating establishing association the system may need to show UI to 287 * collect user confirmation.</p> 288 * 289 * <p>If the app needs to be excluded from battery optimizations (run in the background) 290 * or to have unrestricted data access (use data in the background) it should declare use of 291 * {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and 292 * {@link android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} in its 293 * AndroidManifest.xml respectively. 294 * Note that these special capabilities have a negative effect on the device's battery and 295 * user's data usage, therefore you should request them when absolutely necessary.</p> 296 * 297 * <p>Application can use {@link #getMyAssociations()} for retrieving the list of currently 298 * {@link AssociationInfo} objects, that represent their existing associations. 299 * Applications can also use {@link #disassociate(int)} to remove an association, and are 300 * recommended to do when an association is no longer relevant to avoid unnecessary battery 301 * and/or data drain resulting from special privileges that the association provides</p> 302 * 303 * <p>Calling this API requires a uses-feature 304 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 305 ** 306 * @param request A request object that describes details of the request. 307 * @param callback The callback used to notify application when the association is created. 308 * @param handler The handler which will be used to invoke the callback. 309 * 310 * @see AssociationRequest.Builder 311 * @see #getMyAssociations() 312 * @see #disassociate(int) 313 * @see #associate(AssociationRequest, Executor, Callback) 314 */ 315 @UserHandleAware 316 @RequiresPermission(anyOf = { 317 REQUEST_COMPANION_PROFILE_WATCH, 318 REQUEST_COMPANION_PROFILE_COMPUTER, 319 REQUEST_COMPANION_PROFILE_APP_STREAMING, 320 REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION, 321 }, conditional = true) associate( @onNull AssociationRequest request, @NonNull Callback callback, @Nullable Handler handler)322 public void associate( 323 @NonNull AssociationRequest request, 324 @NonNull Callback callback, 325 @Nullable Handler handler) { 326 if (!checkFeaturePresent()) return; 327 Objects.requireNonNull(request, "Request cannot be null"); 328 Objects.requireNonNull(callback, "Callback cannot be null"); 329 handler = Handler.mainIfNull(handler); 330 331 try { 332 mService.associate(request, new AssociationRequestCallbackProxy(handler, callback), 333 mContext.getOpPackageName(), mContext.getUserId()); 334 } catch (RemoteException e) { 335 throw e.rethrowFromSystemServer(); 336 } 337 } 338 339 /** 340 * Request to associate this app with a companion device. 341 * 342 * <p>Note that before creating establishing association the system may need to show UI to 343 * collect user confirmation.</p> 344 * 345 * <p>If the app needs to be excluded from battery optimizations (run in the background) 346 * or to have unrestricted data access (use data in the background) it should declare use of 347 * {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and 348 * {@link android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} in its 349 * AndroidManifest.xml respectively. 350 * Note that these special capabilities have a negative effect on the device's battery and 351 * user's data usage, therefore you should request them when absolutely necessary.</p> 352 * 353 * <p>Application can use {@link #getMyAssociations()} for retrieving the list of currently 354 * {@link AssociationInfo} objects, that represent their existing associations. 355 * Applications can also use {@link #disassociate(int)} to remove an association, and are 356 * recommended to do when an association is no longer relevant to avoid unnecessary battery 357 * and/or data drain resulting from special privileges that the association provides</p> 358 * 359 * <p>Calling this API requires a uses-feature 360 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 361 ** 362 * @param request A request object that describes details of the request. 363 * @param executor The executor which will be used to invoke the callback. 364 * @param callback The callback used to notify application when the association is created. 365 * 366 * @see AssociationRequest.Builder 367 * @see #getMyAssociations() 368 * @see #disassociate(int) 369 */ 370 @UserHandleAware 371 @RequiresPermission(anyOf = { 372 REQUEST_COMPANION_PROFILE_WATCH, 373 REQUEST_COMPANION_PROFILE_COMPUTER, 374 REQUEST_COMPANION_PROFILE_APP_STREAMING, 375 REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION 376 }, conditional = true) associate( @onNull AssociationRequest request, @NonNull Executor executor, @NonNull Callback callback)377 public void associate( 378 @NonNull AssociationRequest request, 379 @NonNull Executor executor, 380 @NonNull Callback callback) { 381 if (!checkFeaturePresent()) return; 382 Objects.requireNonNull(request, "Request cannot be null"); 383 Objects.requireNonNull(executor, "Executor cannot be null"); 384 Objects.requireNonNull(callback, "Callback cannot be null"); 385 386 try { 387 mService.associate(request, new AssociationRequestCallbackProxy(executor, callback), 388 mContext.getOpPackageName(), mContext.getUserId()); 389 } catch (RemoteException e) { 390 throw e.rethrowFromSystemServer(); 391 } 392 } 393 394 /** 395 * <p>Calling this API requires a uses-feature 396 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 397 * 398 * @return a list of MAC addresses of devices that have been previously associated with the 399 * current app are managed by CompanionDeviceManager (ie. does not include devices managed by 400 * application itself even if they have a MAC address). 401 * 402 * @deprecated use {@link #getMyAssociations()} 403 */ 404 @Deprecated 405 @UserHandleAware 406 @NonNull getAssociations()407 public List<String> getAssociations() { 408 return CollectionUtils.mapNotNull(getMyAssociations(), 409 a -> a.isSelfManaged() ? null : a.getDeviceMacAddressAsString()); 410 } 411 412 /** 413 * <p>Calling this API requires a uses-feature 414 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 415 * 416 * @return a list of associations that have been previously associated with the current app. 417 */ 418 @UserHandleAware 419 @NonNull getMyAssociations()420 public List<AssociationInfo> getMyAssociations() { 421 if (!checkFeaturePresent()) return Collections.emptyList(); 422 423 try { 424 return mService.getAssociations(mContext.getOpPackageName(), mContext.getUserId()); 425 } catch (RemoteException e) { 426 throw e.rethrowFromSystemServer(); 427 } 428 } 429 430 /** 431 * Remove the association between this app and the device with the given mac address. 432 * 433 * <p>Any privileges provided via being associated with a given device will be revoked</p> 434 * 435 * <p>Consider doing so when the 436 * association is no longer relevant to avoid unnecessary battery and/or data drain resulting 437 * from special privileges that the association provides</p> 438 * 439 * <p>Calling this API requires a uses-feature 440 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 441 * 442 * @param deviceMacAddress the MAC address of device to disassociate from this app 443 * 444 * @deprecated use {@link #disassociate(int)} 445 */ 446 @UserHandleAware 447 @Deprecated disassociate(@onNull String deviceMacAddress)448 public void disassociate(@NonNull String deviceMacAddress) { 449 if (!checkFeaturePresent()) return; 450 451 try { 452 mService.legacyDisassociate(deviceMacAddress, mContext.getOpPackageName(), 453 mContext.getUserId()); 454 } catch (RemoteException e) { 455 throw e.rethrowFromSystemServer(); 456 } 457 } 458 459 /** 460 * Remove an association. 461 * 462 * <p>Any privileges provided via being associated with a given device will be revoked</p> 463 * 464 * <p>Calling this API requires a uses-feature 465 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 466 * 467 * @param associationId id of the association to be removed. 468 * 469 * @see #associate(AssociationRequest, Executor, Callback) 470 * @see AssociationInfo#getId() 471 */ 472 @UserHandleAware disassociate(int associationId)473 public void disassociate(int associationId) { 474 if (!checkFeaturePresent()) return; 475 476 try { 477 mService.disassociate(associationId); 478 } catch (RemoteException e) { 479 throw e.rethrowFromSystemServer(); 480 } 481 } 482 483 /** 484 * Request notification access for the given component. 485 * 486 * The given component must follow the protocol specified in {@link NotificationListenerService} 487 * 488 * Only components from the same {@link ComponentName#getPackageName package} as the calling app 489 * are allowed. 490 * 491 * Your app must have an association with a device before calling this API 492 * 493 * <p>Calling this API requires a uses-feature 494 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 495 */ 496 @UserHandleAware requestNotificationAccess(ComponentName component)497 public void requestNotificationAccess(ComponentName component) { 498 if (!checkFeaturePresent()) { 499 return; 500 } 501 try { 502 IntentSender intentSender = mService 503 .requestNotificationAccess(component, mContext.getUserId()) 504 .getIntentSender(); 505 mContext.startIntentSender(intentSender, null, 0, 0, 0); 506 } catch (RemoteException e) { 507 throw e.rethrowFromSystemServer(); 508 } catch (IntentSender.SendIntentException e) { 509 throw new RuntimeException(e); 510 } 511 } 512 513 /** 514 * Check whether the given component can access the notifications via a 515 * {@link NotificationListenerService} 516 * 517 * Your app must have an association with a device before calling this API 518 * 519 * <p>Calling this API requires a uses-feature 520 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 521 * 522 * @param component the name of the component 523 * @return whether the given component has the notification listener permission 524 * 525 * @deprecated Use 526 * {@link NotificationManager#isNotificationListenerAccessGranted(ComponentName)} instead. 527 */ 528 @Deprecated hasNotificationAccess(ComponentName component)529 public boolean hasNotificationAccess(ComponentName component) { 530 if (!checkFeaturePresent()) { 531 return false; 532 } 533 try { 534 return mService.hasNotificationAccess(component); 535 } catch (RemoteException e) { 536 throw e.rethrowFromSystemServer(); 537 } 538 } 539 540 /** 541 * Check if a given package was {@link #associate associated} with a device with given 542 * Wi-Fi MAC address for a given user. 543 * 544 * <p>This is a system API protected by the 545 * {@link android.Manifest.permission#MANAGE_COMPANION_DEVICES} permission, that’s currently 546 * called by the Android Wi-Fi stack to determine whether user consent is required to connect 547 * to a Wi-Fi network. Devices that have been pre-registered as companion devices will not 548 * require user consent to connect.</p> 549 * 550 * <p>Note if the caller has the 551 * {@link android.Manifest.permission#COMPANION_APPROVE_WIFI_CONNECTIONS} permission, this 552 * method will return true by default.</p> 553 * 554 * @param packageName the name of the package that has the association with the companion device 555 * @param macAddress the Wi-Fi MAC address or BSSID of the companion device to check for 556 * @param user the user handle that currently hosts the package being queried for a companion 557 * device association 558 * @return whether a corresponding association record exists 559 * 560 * @hide 561 */ 562 @SystemApi 563 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) isDeviceAssociatedForWifiConnection( @onNull String packageName, @NonNull MacAddress macAddress, @NonNull UserHandle user)564 public boolean isDeviceAssociatedForWifiConnection( 565 @NonNull String packageName, 566 @NonNull MacAddress macAddress, 567 @NonNull UserHandle user) { 568 if (!checkFeaturePresent()) return false; 569 Objects.requireNonNull(packageName, "package name cannot be null"); 570 Objects.requireNonNull(macAddress, "mac address cannot be null"); 571 Objects.requireNonNull(user, "user cannot be null"); 572 try { 573 return mService.isDeviceAssociatedForWifiConnection( 574 packageName, macAddress.toString(), user.getIdentifier()); 575 } catch (RemoteException e) { 576 throw e.rethrowFromSystemServer(); 577 } 578 } 579 580 /** 581 * Gets all package-device {@link AssociationInfo}s for the current user. 582 * 583 * @return the associations list 584 * @see #addOnAssociationsChangedListener(Executor, OnAssociationsChangedListener) 585 * @see #removeOnAssociationsChangedListener(OnAssociationsChangedListener) 586 * @hide 587 */ 588 @SystemApi 589 @UserHandleAware 590 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) getAllAssociations()591 public @NonNull List<AssociationInfo> getAllAssociations() { 592 if (!checkFeaturePresent()) return Collections.emptyList(); 593 try { 594 return mService.getAllAssociationsForUser(mContext.getUserId()); 595 } catch (RemoteException e) { 596 throw e.rethrowFromSystemServer(); 597 } 598 } 599 600 /** 601 * Listener for any changes to {@link AssociationInfo}. 602 * 603 * @hide 604 */ 605 @SystemApi 606 public interface OnAssociationsChangedListener { 607 /** 608 * Invoked when a change occurs to any of the associations for the user (including adding 609 * new associations and removing existing associations). 610 * 611 * @param associations all existing associations for the user (after the change). 612 */ onAssociationsChanged(@onNull List<AssociationInfo> associations)613 void onAssociationsChanged(@NonNull List<AssociationInfo> associations); 614 } 615 616 /** 617 * Register listener for any changes to {@link AssociationInfo}. 618 * 619 * @see #getAllAssociations() 620 * @hide 621 */ 622 @SystemApi 623 @UserHandleAware 624 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) addOnAssociationsChangedListener( @onNull Executor executor, @NonNull OnAssociationsChangedListener listener)625 public void addOnAssociationsChangedListener( 626 @NonNull Executor executor, @NonNull OnAssociationsChangedListener listener) { 627 if (!checkFeaturePresent()) return; 628 synchronized (mListeners) { 629 final OnAssociationsChangedListenerProxy proxy = new OnAssociationsChangedListenerProxy( 630 executor, listener); 631 try { 632 mService.addOnAssociationsChangedListener(proxy, mContext.getUserId()); 633 } catch (RemoteException e) { 634 throw e.rethrowFromSystemServer(); 635 } 636 mListeners.add(proxy); 637 } 638 } 639 640 /** 641 * Unregister listener for any changes to {@link AssociationInfo}. 642 * 643 * @see #getAllAssociations() 644 * @hide 645 */ 646 @SystemApi 647 @UserHandleAware 648 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) removeOnAssociationsChangedListener( @onNull OnAssociationsChangedListener listener)649 public void removeOnAssociationsChangedListener( 650 @NonNull OnAssociationsChangedListener listener) { 651 if (!checkFeaturePresent()) return; 652 synchronized (mListeners) { 653 final Iterator<OnAssociationsChangedListenerProxy> iterator = mListeners.iterator(); 654 while (iterator.hasNext()) { 655 final OnAssociationsChangedListenerProxy proxy = iterator.next(); 656 if (proxy.mListener == listener) { 657 try { 658 mService.removeOnAssociationsChangedListener(proxy, mContext.getUserId()); 659 } catch (RemoteException e) { 660 throw e.rethrowFromSystemServer(); 661 } 662 iterator.remove(); 663 } 664 } 665 } 666 } 667 668 /** 669 * Checks whether the bluetooth device represented by the mac address was recently associated 670 * with the companion app. This allows these devices to skip the Bluetooth pairing dialog if 671 * their pairing variant is {@link BluetoothDevice#PAIRING_VARIANT_CONSENT}. 672 * 673 * @param packageName the package name of the calling app 674 * @param deviceMacAddress the bluetooth device's mac address 675 * @param user the user handle that currently hosts the package being queried for a companion 676 * device association 677 * @return true if it was recently associated and we can bypass the dialog, false otherwise 678 * @hide 679 */ 680 @SystemApi 681 @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) canPairWithoutPrompt(@onNull String packageName, @NonNull String deviceMacAddress, @NonNull UserHandle user)682 public boolean canPairWithoutPrompt(@NonNull String packageName, 683 @NonNull String deviceMacAddress, @NonNull UserHandle user) { 684 if (!checkFeaturePresent()) { 685 return false; 686 } 687 Objects.requireNonNull(packageName, "package name cannot be null"); 688 Objects.requireNonNull(deviceMacAddress, "device mac address cannot be null"); 689 Objects.requireNonNull(user, "user handle cannot be null"); 690 try { 691 return mService.canPairWithoutPrompt(packageName, deviceMacAddress, 692 user.getIdentifier()); 693 } catch (RemoteException e) { 694 throw e.rethrowFromSystemServer(); 695 } 696 } 697 698 /** 699 * Register to receive callbacks whenever the associated device comes in and out of range. 700 * 701 * The provided device must be {@link #associate associated} with the calling app before 702 * calling this method. 703 * 704 * Caller must implement a single {@link CompanionDeviceService} which will be bound to and 705 * receive callbacks to {@link CompanionDeviceService#onDeviceAppeared} and 706 * {@link CompanionDeviceService#onDeviceDisappeared}. 707 * The app doesn't need to remain running in order to receive its callbacks. 708 * 709 * Calling app must declare uses-permission 710 * {@link android.Manifest.permission#REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE}. 711 * 712 * Calling app must check for feature presence of 713 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API. 714 * 715 * For Bluetooth LE devices this is based on scanning for device with the given address. 716 * For Bluetooth classic devices this is triggered when the device connects/disconnects. 717 * WiFi devices are not supported. 718 * 719 * If a Bluetooth LE device wants to use a rotating mac address, it is recommended to use 720 * Resolvable Private Address, and ensure the device is bonded to the phone so that android OS 721 * is able to resolve the address. 722 * 723 * @param deviceAddress a previously-associated companion device's address 724 * 725 * @throws DeviceNotAssociatedException if the given device was not previously associated 726 * with this app. 727 */ 728 @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) startObservingDevicePresence(@onNull String deviceAddress)729 public void startObservingDevicePresence(@NonNull String deviceAddress) 730 throws DeviceNotAssociatedException { 731 if (!checkFeaturePresent()) { 732 return; 733 } 734 Objects.requireNonNull(deviceAddress, "address cannot be null"); 735 try { 736 mService.registerDevicePresenceListenerService(deviceAddress, 737 mContext.getOpPackageName(), mContext.getUserId()); 738 } catch (RemoteException e) { 739 ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); 740 throw e.rethrowFromSystemServer(); 741 } 742 } 743 744 /** 745 * Unregister for receiving callbacks whenever the associated device comes in and out of range. 746 * 747 * The provided device must be {@link #associate associated} with the calling app before 748 * calling this method. 749 * 750 * Calling app must declare uses-permission 751 * {@link android.Manifest.permission#REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE}. 752 * 753 * Calling app must check for feature presence of 754 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API. 755 * 756 * @param deviceAddress a previously-associated companion device's address 757 * 758 * @throws DeviceNotAssociatedException if the given device was not previously associated 759 * with this app. 760 */ 761 @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) stopObservingDevicePresence(@onNull String deviceAddress)762 public void stopObservingDevicePresence(@NonNull String deviceAddress) 763 throws DeviceNotAssociatedException { 764 if (!checkFeaturePresent()) { 765 return; 766 } 767 Objects.requireNonNull(deviceAddress, "address cannot be null"); 768 try { 769 mService.unregisterDevicePresenceListenerService(deviceAddress, 770 mContext.getPackageName(), mContext.getUserId()); 771 } catch (RemoteException e) { 772 ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); 773 } 774 } 775 776 /** 777 * Dispatch a message to system for processing. 778 * 779 * <p>Calling app must declare uses-permission 780 * {@link android.Manifest.permission#DELIVER_COMPANION_MESSAGES}</p> 781 * 782 * @param messageId id of the message 783 * @param associationId association id of the associated device where data is coming from 784 * @param message message received from the associated device 785 * 786 * @throws DeviceNotAssociatedException if the given device was not previously associated with 787 * this app 788 * 789 * @hide 790 */ 791 @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) dispatchMessage(int messageId, int associationId, @NonNull byte[] message)792 public void dispatchMessage(int messageId, int associationId, @NonNull byte[] message) 793 throws DeviceNotAssociatedException { 794 try { 795 mService.dispatchMessage(messageId, associationId, message); 796 } catch (RemoteException e) { 797 ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); 798 throw e.rethrowFromSystemServer(); 799 } 800 } 801 802 /** 803 * Associates given device with given app for the given user directly, without UI prompt. 804 * 805 * @param packageName package name of the companion app 806 * @param macAddress mac address of the device to associate 807 * @param certificate The SHA256 digest of the companion app's signing certificate 808 * 809 * @hide 810 */ 811 @SystemApi 812 @RequiresPermission(android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES) associate( @onNull String packageName, @NonNull MacAddress macAddress, @NonNull byte[] certificate)813 public void associate( 814 @NonNull String packageName, 815 @NonNull MacAddress macAddress, 816 @NonNull byte[] certificate) { 817 if (!checkFeaturePresent()) { 818 return; 819 } 820 Objects.requireNonNull(packageName, "package name cannot be null"); 821 Objects.requireNonNull(macAddress, "mac address cannot be null"); 822 823 UserHandle user = android.os.Process.myUserHandle(); 824 try { 825 mService.createAssociation( 826 packageName, macAddress.toString(), user.getIdentifier(), certificate); 827 } catch (RemoteException e) { 828 throw e.rethrowFromSystemServer(); 829 } 830 } 831 832 /** 833 * Notify the system that the given self-managed association has just appeared. 834 * This causes the system to bind to the companion app to keep it running until the association 835 * is reported as disappeared 836 * 837 * <p>This API is only available for the companion apps that manage the connectivity by 838 * themselves.</p> 839 * 840 * @param associationId the unique {@link AssociationInfo#getId ID} assigned to the Association 841 * recorded by CompanionDeviceManager 842 * 843 * @hide 844 */ 845 @SystemApi 846 @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) notifyDeviceAppeared(int associationId)847 public void notifyDeviceAppeared(int associationId) { 848 try { 849 mService.notifyDeviceAppeared(associationId); 850 } catch (RemoteException e) { 851 throw e.rethrowFromSystemServer(); 852 } 853 } 854 855 /** 856 * Notify the system that the given self-managed association has just disappeared. 857 * This causes the system to unbind to the companion app. 858 * 859 * <p>This API is only available for the companion apps that manage the connectivity by 860 * themselves.</p> 861 * 862 * @param associationId the unique {@link AssociationInfo#getId ID} assigned to the Association 863 * recorded by CompanionDeviceManager 864 865 * @hide 866 */ 867 @SystemApi 868 @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) notifyDeviceDisappeared(int associationId)869 public void notifyDeviceDisappeared(int associationId) { 870 try { 871 mService.notifyDeviceDisappeared(associationId); 872 } catch (RemoteException e) { 873 throw e.rethrowFromSystemServer(); 874 } 875 } 876 checkFeaturePresent()877 private boolean checkFeaturePresent() { 878 boolean featurePresent = mService != null; 879 if (!featurePresent && DEBUG) { 880 Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP 881 + " not available"); 882 } 883 return featurePresent; 884 } 885 886 private static class AssociationRequestCallbackProxy extends IAssociationRequestCallback.Stub { 887 private final Handler mHandler; 888 private final Callback mCallback; 889 private final Executor mExecutor; 890 AssociationRequestCallbackProxy( @onNull Executor executor, @NonNull Callback callback)891 private AssociationRequestCallbackProxy( 892 @NonNull Executor executor, @NonNull Callback callback) { 893 mExecutor = executor; 894 mHandler = null; 895 mCallback = callback; 896 } 897 AssociationRequestCallbackProxy( @onNull Handler handler, @NonNull Callback callback)898 private AssociationRequestCallbackProxy( 899 @NonNull Handler handler, @NonNull Callback callback) { 900 mHandler = handler; 901 mExecutor = null; 902 mCallback = callback; 903 } 904 905 @Override onAssociationPending(@onNull PendingIntent pi)906 public void onAssociationPending(@NonNull PendingIntent pi) { 907 execute(mCallback::onAssociationPending, pi.getIntentSender()); 908 } 909 910 @Override onAssociationCreated(@onNull AssociationInfo association)911 public void onAssociationCreated(@NonNull AssociationInfo association) { 912 execute(mCallback::onAssociationCreated, association); 913 } 914 915 @Override onFailure(CharSequence error)916 public void onFailure(CharSequence error) throws RemoteException { 917 execute(mCallback::onFailure, error); 918 } 919 execute(Consumer<T> callback, T arg)920 private <T> void execute(Consumer<T> callback, T arg) { 921 if (mExecutor != null) { 922 mExecutor.execute(() -> callback.accept(arg)); 923 } else { 924 mHandler.post(() -> callback.accept(arg)); 925 } 926 } 927 } 928 929 private static class OnAssociationsChangedListenerProxy 930 extends IOnAssociationsChangedListener.Stub { 931 private final Executor mExecutor; 932 private final OnAssociationsChangedListener mListener; 933 OnAssociationsChangedListenerProxy(Executor executor, OnAssociationsChangedListener listener)934 private OnAssociationsChangedListenerProxy(Executor executor, 935 OnAssociationsChangedListener listener) { 936 mExecutor = executor; 937 mListener = listener; 938 } 939 940 @Override onAssociationsChanged(@onNull List<AssociationInfo> associations)941 public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) { 942 mExecutor.execute(() -> mListener.onAssociationsChanged(associations)); 943 } 944 } 945 } 946