blob: 1a5de5690caeaea3355ac8d0a789c4dc2ef4b4de [file] [log] [blame]
Eugene Susla6ed45d82017-01-22 13:52:51 -08001/*
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
17package android.companion;
18
19
Eugene Suslaa38fbf62017-03-14 10:26:10 -070020import static com.android.internal.util.Preconditions.checkNotNull;
21
Eugene Susla6ed45d82017-01-22 13:52:51 -080022import android.annotation.NonNull;
23import android.annotation.Nullable;
Jeff Sharkeyd86b8fe2017-06-02 17:36:26 -060024import android.annotation.SystemService;
Eugene Susla6fd0ce32017-04-24 16:13:20 -070025import android.app.Activity;
26import android.app.Application;
Eugene Susla6ed45d82017-01-22 13:52:51 -080027import android.app.PendingIntent;
Eugene Suslacf00ade2017-04-10 11:51:58 -070028import android.content.ComponentName;
Eugene Susla6ed45d82017-01-22 13:52:51 -080029import android.content.Context;
30import android.content.IntentSender;
Eugene Susla7c3eef22017-03-10 14:25:58 -080031import android.content.pm.PackageManager;
Eugene Susla6fd0ce32017-04-24 16:13:20 -070032import android.os.Bundle;
Eugene Susla6ed45d82017-01-22 13:52:51 -080033import android.os.Handler;
Eugene Susla6ed45d82017-01-22 13:52:51 -080034import android.os.RemoteException;
Eugene Suslacf00ade2017-04-10 11:51:58 -070035import android.service.notification.NotificationListenerService;
Eugene Susla7c3eef22017-03-10 14:25:58 -080036import android.util.Log;
Eugene Susla6ed45d82017-01-22 13:52:51 -080037
Eugene Susla7c3eef22017-03-10 14:25:58 -080038import java.util.Collections;
Eugene Susla47aafbe2017-02-13 12:46:46 -080039import java.util.List;
Eugene Susla08727b02017-07-12 11:20:31 -070040import java.util.function.BiConsumer;
Eugene Susla47aafbe2017-02-13 12:46:46 -080041
Eugene Susla6ed45d82017-01-22 13:52:51 -080042/**
43 * System level service for managing companion devices
44 *
Svet Ganovda0acdf2017-02-15 10:28:51 -080045 * <p>To obtain an instance call {@link Context#getSystemService}({@link
46 * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest,
47 * Callback, Handler)} to initiate the flow of associating current package with a
48 * device selected by user.</p>
Eugene Susla6ed45d82017-01-22 13:52:51 -080049 *
50 * @see AssociationRequest
51 */
Jeff Sharkeyd86b8fe2017-06-02 17:36:26 -060052@SystemService(Context.COMPANION_DEVICE_SERVICE)
Eugene Susla6ed45d82017-01-22 13:52:51 -080053public final class CompanionDeviceManager {
54
Eugene Suslaa38fbf62017-03-14 10:26:10 -070055 private static final boolean DEBUG = false;
Eugene Susla7c3eef22017-03-10 14:25:58 -080056 private static final String LOG_TAG = "CompanionDeviceManager";
57
Eugene Susla6ed45d82017-01-22 13:52:51 -080058 /**
59 * A device, returned in the activity result of the {@link IntentSender} received in
60 * {@link Callback#onDeviceFound}
61 */
62 public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
63
64 /**
Svet Ganovda0acdf2017-02-15 10:28:51 -080065 * The package name of the companion device discovery component.
66 *
67 * @hide
68 */
69 public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME =
70 "com.android.companiondevicemanager";
71
72 /**
Eugene Susla6ed45d82017-01-22 13:52:51 -080073 * A callback to receive once at least one suitable device is found, or the search failed
74 * (e.g. timed out)
75 */
76 public abstract static class Callback {
77
78 /**
79 * Called once at least one suitable device is found
80 *
81 * @param chooserLauncher a {@link IntentSender} to launch the UI for user to select a
82 * device
83 */
84 public abstract void onDeviceFound(IntentSender chooserLauncher);
85
86 /**
87 * Called if there was an error looking for device(s), e.g. timeout
88 *
89 * @param error the cause of the error
90 */
91 public abstract void onFailure(CharSequence error);
92 }
93
94 private final ICompanionDeviceManager mService;
95 private final Context mContext;
96
97 /** @hide */
98 public CompanionDeviceManager(
Eugene Susla7c3eef22017-03-10 14:25:58 -080099 @Nullable ICompanionDeviceManager service, @NonNull Context context) {
Eugene Susla6ed45d82017-01-22 13:52:51 -0800100 mService = service;
101 mContext = context;
102 }
103
104 /**
105 * Associate this app with a companion device, selected by user
106 *
Svet Ganovda0acdf2017-02-15 10:28:51 -0800107 * <p>Once at least one appropriate device is found, {@code callback} will be called with a
Eugene Susla6ed45d82017-01-22 13:52:51 -0800108 * {@link PendingIntent} that can be used to show the list of available devices for the user
109 * to select.
110 * It should be started for result (i.e. using
111 * {@link android.app.Activity#startIntentSenderForResult}), as the resulting
112 * {@link android.content.Intent} will contain extra {@link #EXTRA_DEVICE}, with the selected
Svet Ganovda0acdf2017-02-15 10:28:51 -0800113 * device. (e.g. {@link android.bluetooth.BluetoothDevice})</p>
114 *
115 * <p>If your app needs to be excluded from battery optimizations (run in the background)
116 * or to have unrestricted data access (use data in the background) you can declare that
Jeff Sharkey67f9d502017-08-05 13:49:13 -0600117 * you use the {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and {@link
118 * android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} respectively. Note that these
Svet Ganovda0acdf2017-02-15 10:28:51 -0800119 * special capabilities have a negative effect on the device's battery and user's data
120 * usage, therefore you should requested them when absolutely necessary.</p>
Eugene Susla6ed45d82017-01-22 13:52:51 -0800121 *
Eugene Susla47aafbe2017-02-13 12:46:46 -0800122 * <p>You can call {@link #getAssociations} to get the list of currently associated
123 * devices, and {@link #disassociate} to remove an association. Consider doing so when the
124 * association is no longer relevant to avoid unnecessary battery and/or data drain resulting
125 * from special privileges that the association provides</p>
126 *
Eugene Suslab0f97402017-04-24 13:26:12 -0700127 * <p>Calling this API requires a uses-feature
128 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
129 *
Eugene Susla6ed45d82017-01-22 13:52:51 -0800130 * @param request specific details about this request
131 * @param callback will be called once there's at least one device found for user to choose from
132 * @param handler A handler to control which thread the callback will be delivered on, or null,
133 * to deliver it on main thread
134 *
135 * @see AssociationRequest
136 */
137 public void associate(
Eugene Susla36e866b2017-02-23 18:24:39 -0800138 @NonNull AssociationRequest request,
Eugene Susla6ed45d82017-01-22 13:52:51 -0800139 @NonNull Callback callback,
140 @Nullable Handler handler) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800141 if (!checkFeaturePresent()) {
142 return;
143 }
Eugene Suslaa38fbf62017-03-14 10:26:10 -0700144 checkNotNull(request, "Request cannot be null");
145 checkNotNull(callback, "Callback cannot be null");
Eugene Susla6ed45d82017-01-22 13:52:51 -0800146 try {
147 mService.associate(
148 request,
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700149 new CallbackProxy(request, callback, Handler.mainIfNull(handler)),
150 getCallingPackage());
Eugene Susla6ed45d82017-01-22 13:52:51 -0800151 } catch (RemoteException e) {
152 throw e.rethrowFromSystemServer();
153 }
154 }
155
Eugene Susla47aafbe2017-02-13 12:46:46 -0800156 /**
Eugene Suslab0f97402017-04-24 13:26:12 -0700157 * <p>Calling this API requires a uses-feature
158 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
159 *
Eugene Susla47aafbe2017-02-13 12:46:46 -0800160 * @return a list of MAC addresses of devices that have been previously associated with the
161 * current app. You can use these with {@link #disassociate}
162 */
163 @NonNull
164 public List<String> getAssociations() {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800165 if (!checkFeaturePresent()) {
166 return Collections.emptyList();
167 }
Eugene Susla47aafbe2017-02-13 12:46:46 -0800168 try {
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700169 return mService.getAssociations(getCallingPackage(), mContext.getUserId());
Eugene Susla47aafbe2017-02-13 12:46:46 -0800170 } catch (RemoteException e) {
171 throw e.rethrowFromSystemServer();
172 }
173 }
174
175 /**
176 * Remove the association between this app and the device with the given mac address.
177 *
178 * <p>Any privileges provided via being associated with a given device will be revoked</p>
179 *
180 * <p>Consider doing so when the
181 * association is no longer relevant to avoid unnecessary battery and/or data drain resulting
182 * from special privileges that the association provides</p>
183 *
Eugene Suslab0f97402017-04-24 13:26:12 -0700184 * <p>Calling this API requires a uses-feature
185 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
186 *
Eugene Susla47aafbe2017-02-13 12:46:46 -0800187 * @param deviceMacAddress the MAC address of device to disassociate from this app
188 */
189 public void disassociate(@NonNull String deviceMacAddress) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800190 if (!checkFeaturePresent()) {
191 return;
192 }
Eugene Susla47aafbe2017-02-13 12:46:46 -0800193 try {
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700194 mService.disassociate(deviceMacAddress, getCallingPackage());
Eugene Susla47aafbe2017-02-13 12:46:46 -0800195 } catch (RemoteException e) {
196 throw e.rethrowFromSystemServer();
197 }
198 }
199
Eugene Suslacf00ade2017-04-10 11:51:58 -0700200 /**
201 * Request notification access for the given component.
202 *
203 * The given component must follow the protocol specified in {@link NotificationListenerService}
204 *
205 * Only components from the same {@link ComponentName#getPackageName package} as the calling app
206 * are allowed.
207 *
208 * Your app must have an association with a device before calling this API
Eugene Suslab0f97402017-04-24 13:26:12 -0700209 *
210 * <p>Calling this API requires a uses-feature
211 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
Eugene Suslacf00ade2017-04-10 11:51:58 -0700212 */
213 public void requestNotificationAccess(ComponentName component) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800214 if (!checkFeaturePresent()) {
215 return;
216 }
Eugene Suslacf00ade2017-04-10 11:51:58 -0700217 try {
Eugene Suslad149a082017-06-19 17:27:23 -0700218 IntentSender intentSender = mService.requestNotificationAccess(component)
219 .getIntentSender();
220 mContext.startIntentSender(intentSender, null, 0, 0, 0);
Eugene Suslacf00ade2017-04-10 11:51:58 -0700221 } catch (RemoteException e) {
222 throw e.rethrowFromSystemServer();
Eugene Suslad149a082017-06-19 17:27:23 -0700223 } catch (IntentSender.SendIntentException e) {
Eugene Suslacf00ade2017-04-10 11:51:58 -0700224 throw new RuntimeException(e);
225 }
Eugene Susla6ed45d82017-01-22 13:52:51 -0800226 }
227
Eugene Suslacf00ade2017-04-10 11:51:58 -0700228 /**
229 * Check whether the given component can access the notifications via a
230 * {@link NotificationListenerService}
231 *
232 * Your app must have an association with a device before calling this API
233 *
Eugene Suslab0f97402017-04-24 13:26:12 -0700234 * <p>Calling this API requires a uses-feature
235 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
236 *
Eugene Suslacf00ade2017-04-10 11:51:58 -0700237 * @param component the name of the component
238 * @return whether the given component has the notification listener permission
239 */
240 public boolean hasNotificationAccess(ComponentName component) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800241 if (!checkFeaturePresent()) {
242 return false;
243 }
Eugene Suslacf00ade2017-04-10 11:51:58 -0700244 try {
245 return mService.hasNotificationAccess(component);
246 } catch (RemoteException e) {
247 throw e.rethrowFromSystemServer();
248 }
Eugene Susla6ed45d82017-01-22 13:52:51 -0800249 }
250
Eugene Susla7c3eef22017-03-10 14:25:58 -0800251 private boolean checkFeaturePresent() {
Eugene Suslad7ff1772017-03-24 13:35:45 -0700252 boolean featurePresent = mService != null;
Eugene Susla7c3eef22017-03-10 14:25:58 -0800253 if (!featurePresent && DEBUG) {
254 Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP
255 + " not available");
256 }
257 return featurePresent;
258 }
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700259
260 private Activity getActivity() {
261 return (Activity) mContext;
262 }
263
264 private String getCallingPackage() {
265 return mContext.getPackageName();
266 }
267
268 private class CallbackProxy extends IFindDeviceCallback.Stub
269 implements Application.ActivityLifecycleCallbacks {
270
271 private Callback mCallback;
272 private Handler mHandler;
273 private AssociationRequest mRequest;
274
Eugene Susla08727b02017-07-12 11:20:31 -0700275 final Object mLock = new Object();
276
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700277 private CallbackProxy(AssociationRequest request, Callback callback, Handler handler) {
278 mCallback = callback;
279 mHandler = handler;
280 mRequest = request;
281 getActivity().getApplication().registerActivityLifecycleCallbacks(this);
282 }
283
284 @Override
285 public void onSuccess(PendingIntent launcher) {
Eugene Susla08727b02017-07-12 11:20:31 -0700286 lockAndPost(Callback::onDeviceFound, launcher.getIntentSender());
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700287 }
288
289 @Override
290 public void onFailure(CharSequence reason) {
Eugene Susla08727b02017-07-12 11:20:31 -0700291 lockAndPost(Callback::onFailure, reason);
292 }
293
294 <T> void lockAndPost(BiConsumer<Callback, T> action, T payload) {
295 synchronized (mLock) {
296 if (mHandler != null) {
297 mHandler.post(() -> {
298 Callback callback = null;
299 synchronized (mLock) {
300 callback = mCallback;
301 }
302 if (callback != null) {
303 action.accept(callback, payload);
304 }
305 });
306 }
307 }
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700308 }
309
310 @Override
311 public void onActivityDestroyed(Activity activity) {
Eugene Susla08727b02017-07-12 11:20:31 -0700312 synchronized (mLock) {
313 if (activity != getActivity()) return;
314 try {
315 mService.stopScan(mRequest, this, getCallingPackage());
316 } catch (RemoteException e) {
317 e.rethrowFromSystemServer();
318 }
319 getActivity().getApplication().unregisterActivityLifecycleCallbacks(this);
320 mCallback = null;
321 mHandler = null;
322 mRequest = null;
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700323 }
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700324 }
325
326 @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
327 @Override public void onActivityStarted(Activity activity) {}
328 @Override public void onActivityResumed(Activity activity) {}
329 @Override public void onActivityPaused(Activity activity) {}
330 @Override public void onActivityStopped(Activity activity) {}
331 @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
332 }
Eugene Susla6ed45d82017-01-22 13:52:51 -0800333}