• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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