• 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 
20 import static com.android.internal.util.Preconditions.checkNotNull;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.SystemService;
25 import android.app.Activity;
26 import android.app.Application;
27 import android.app.PendingIntent;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.IntentSender;
31 import android.content.pm.PackageManager;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.RemoteException;
35 import android.service.notification.NotificationListenerService;
36 import android.util.Log;
37 
38 import java.util.Collections;
39 import java.util.List;
40 
41 /**
42  * System level service for managing companion devices
43  *
44  * <p>To obtain an instance call {@link Context#getSystemService}({@link
45  * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest,
46  * Callback, Handler)} to initiate the flow of associating current package with a
47  * device selected by user.</p>
48  *
49  * @see AssociationRequest
50  */
51 @SystemService(Context.COMPANION_DEVICE_SERVICE)
52 public final class CompanionDeviceManager {
53 
54     private static final boolean DEBUG = false;
55     private static final String LOG_TAG = "CompanionDeviceManager";
56 
57     /**
58      * A device, returned in the activity result of the {@link IntentSender} received in
59      * {@link Callback#onDeviceFound}
60      */
61     public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
62 
63     /**
64      * The package name of the companion device discovery component.
65      *
66      * @hide
67      */
68     public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME =
69             "com.android.companiondevicemanager";
70 
71     /**
72      * A callback to receive once at least one suitable device is found, or the search failed
73      * (e.g. timed out)
74      */
75     public abstract static class Callback {
76 
77         /**
78          * Called once at least one suitable device is found
79          *
80          * @param chooserLauncher a {@link IntentSender} to launch the UI for user to select a
81          *                        device
82          */
onDeviceFound(IntentSender chooserLauncher)83         public abstract void onDeviceFound(IntentSender chooserLauncher);
84 
85         /**
86          * Called if there was an error looking for device(s), e.g. timeout
87          *
88          * @param error the cause of the error
89          */
onFailure(CharSequence error)90         public abstract void onFailure(CharSequence error);
91     }
92 
93     private final ICompanionDeviceManager mService;
94     private final Context mContext;
95 
96     /** @hide */
CompanionDeviceManager( @ullable ICompanionDeviceManager service, @NonNull Context context)97     public CompanionDeviceManager(
98             @Nullable ICompanionDeviceManager service, @NonNull Context context) {
99         mService = service;
100         mContext = context;
101     }
102 
103     /**
104      * Associate this app with a companion device, selected by user
105      *
106      * <p>Once at least one appropriate device is found, {@code callback} will be called with a
107      * {@link PendingIntent} that can be used to show the list of available devices for the user
108      * to select.
109      * It should be started for result (i.e. using
110      * {@link android.app.Activity#startIntentSenderForResult}), as the resulting
111      * {@link android.content.Intent} will contain extra {@link #EXTRA_DEVICE}, with the selected
112      * device. (e.g. {@link android.bluetooth.BluetoothDevice})</p>
113      *
114      * <p>If your app needs to be excluded from battery optimizations (run in the background)
115      * or to have unrestricted data access (use data in the background) you can declare that
116      * you use the {@link android.Manifest.permission#RUN_IN_BACKGROUND} and {@link
117      * android.Manifest.permission#USE_DATA_IN_BACKGROUND} respectively. Note that these
118      * special capabilities have a negative effect on the device's battery and user's data
119      * usage, therefore you should requested them when absolutely necessary.</p>
120      *
121      * <p>You can call {@link #getAssociations} to get the list of currently associated
122      * devices, and {@link #disassociate} to remove an association. Consider doing so when the
123      * association is no longer relevant to avoid unnecessary battery and/or data drain resulting
124      * from special privileges that the association provides</p>
125      *
126      * <p>Calling this API requires a uses-feature
127      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
128      *
129      * @param request specific details about this request
130      * @param callback will be called once there's at least one device found for user to choose from
131      * @param handler A handler to control which thread the callback will be delivered on, or null,
132      *                to deliver it on main thread
133      *
134      * @see AssociationRequest
135      */
associate( @onNull AssociationRequest request, @NonNull Callback callback, @Nullable Handler handler)136     public void associate(
137             @NonNull AssociationRequest request,
138             @NonNull Callback callback,
139             @Nullable Handler handler) {
140         if (!checkFeaturePresent()) {
141             return;
142         }
143         checkNotNull(request, "Request cannot be null");
144         checkNotNull(callback, "Callback cannot be null");
145         try {
146             mService.associate(
147                     request,
148                     new CallbackProxy(request, callback, Handler.mainIfNull(handler)),
149                     getCallingPackage());
150         } catch (RemoteException e) {
151             throw e.rethrowFromSystemServer();
152         }
153     }
154 
155     /**
156      * <p>Calling this API requires a uses-feature
157      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
158      *
159      * @return a list of MAC addresses of devices that have been previously associated with the
160      * current app. You can use these with {@link #disassociate}
161      */
162     @NonNull
getAssociations()163     public List<String> getAssociations() {
164         if (!checkFeaturePresent()) {
165             return Collections.emptyList();
166         }
167         try {
168             return mService.getAssociations(getCallingPackage(), mContext.getUserId());
169         } catch (RemoteException e) {
170             throw e.rethrowFromSystemServer();
171         }
172     }
173 
174     /**
175      * Remove the association between this app and the device with the given mac address.
176      *
177      * <p>Any privileges provided via being associated with a given device will be revoked</p>
178      *
179      * <p>Consider doing so when the
180      * association is no longer relevant to avoid unnecessary battery and/or data drain resulting
181      * from special privileges that the association provides</p>
182      *
183      * <p>Calling this API requires a uses-feature
184      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
185      *
186      * @param deviceMacAddress the MAC address of device to disassociate from this app
187      */
disassociate(@onNull String deviceMacAddress)188     public void disassociate(@NonNull String deviceMacAddress) {
189         if (!checkFeaturePresent()) {
190             return;
191         }
192         try {
193             mService.disassociate(deviceMacAddress, getCallingPackage());
194         } catch (RemoteException e) {
195             throw e.rethrowFromSystemServer();
196         }
197     }
198 
199     /**
200      * Request notification access for the given component.
201      *
202      * The given component must follow the protocol specified in {@link NotificationListenerService}
203      *
204      * Only components from the same {@link ComponentName#getPackageName package} as the calling app
205      * are allowed.
206      *
207      * Your app must have an association with a device before calling this API
208      *
209      * <p>Calling this API requires a uses-feature
210      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
211      */
requestNotificationAccess(ComponentName component)212     public void requestNotificationAccess(ComponentName component) {
213         if (!checkFeaturePresent()) {
214             return;
215         }
216         try {
217             IntentSender intentSender = mService.requestNotificationAccess(component)
218                     .getIntentSender();
219             mContext.startIntentSender(intentSender, null, 0, 0, 0);
220         } catch (RemoteException e) {
221             throw e.rethrowFromSystemServer();
222         } catch (IntentSender.SendIntentException e) {
223             throw new RuntimeException(e);
224         }
225     }
226 
227     /**
228      * Check whether the given component can access the notifications via a
229      * {@link NotificationListenerService}
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      *
236      * @param component the name of the component
237      * @return whether the given component has the notification listener permission
238      */
hasNotificationAccess(ComponentName component)239     public boolean hasNotificationAccess(ComponentName component) {
240         if (!checkFeaturePresent()) {
241             return false;
242         }
243         try {
244             return mService.hasNotificationAccess(component);
245         } catch (RemoteException e) {
246             throw e.rethrowFromSystemServer();
247         }
248     }
249 
checkFeaturePresent()250     private boolean checkFeaturePresent() {
251         boolean featurePresent = mService != null;
252         if (!featurePresent && DEBUG) {
253             Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP
254                     + " not available");
255         }
256         return featurePresent;
257     }
258 
getActivity()259     private Activity getActivity() {
260         return (Activity) mContext;
261     }
262 
getCallingPackage()263     private String getCallingPackage() {
264         return mContext.getPackageName();
265     }
266 
267     private class CallbackProxy extends IFindDeviceCallback.Stub
268             implements Application.ActivityLifecycleCallbacks {
269 
270         private Callback mCallback;
271         private Handler mHandler;
272         private AssociationRequest mRequest;
273 
CallbackProxy(AssociationRequest request, Callback callback, Handler handler)274         private CallbackProxy(AssociationRequest request, Callback callback, Handler handler) {
275             mCallback = callback;
276             mHandler = handler;
277             mRequest = request;
278             getActivity().getApplication().registerActivityLifecycleCallbacks(this);
279         }
280 
281         @Override
onSuccess(PendingIntent launcher)282         public void onSuccess(PendingIntent launcher) {
283             mHandler.post(() -> mCallback.onDeviceFound(launcher.getIntentSender()));
284         }
285 
286         @Override
onFailure(CharSequence reason)287         public void onFailure(CharSequence reason) {
288             mHandler.post(() -> mCallback.onFailure(reason));
289         }
290 
291         @Override
onActivityDestroyed(Activity activity)292         public void onActivityDestroyed(Activity activity) {
293             if (activity != getActivity()) return;
294             try {
295                 mService.stopScan(mRequest, this, getCallingPackage());
296             } catch (RemoteException e) {
297                 e.rethrowFromSystemServer();
298             }
299             getActivity().getApplication().unregisterActivityLifecycleCallbacks(this);
300             mCallback = null;
301             mHandler = null;
302             mRequest = null;
303         }
304 
onActivityCreated(Activity activity, Bundle savedInstanceState)305         @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
onActivityStarted(Activity activity)306         @Override public void onActivityStarted(Activity activity) {}
onActivityResumed(Activity activity)307         @Override public void onActivityResumed(Activity activity) {}
onActivityPaused(Activity activity)308         @Override public void onActivityPaused(Activity activity) {}
onActivityStopped(Activity activity)309         @Override public void onActivityStopped(Activity activity) {}
onActivitySaveInstanceState(Activity activity, Bundle outState)310         @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
311     }
312 }
313