• 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 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