blob: b2952aab233499ed371c728b5cff7fdc1478f64a [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;
40
Eugene Susla6ed45d82017-01-22 13:52:51 -080041/**
42 * System level service for managing companion devices
43 *
Svet Ganovda0acdf2017-02-15 10:28:51 -080044 * <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>
Eugene Susla6ed45d82017-01-22 13:52:51 -080048 *
49 * @see AssociationRequest
50 */
Jeff Sharkeyd86b8fe2017-06-02 17:36:26 -060051@SystemService(Context.COMPANION_DEVICE_SERVICE)
Eugene Susla6ed45d82017-01-22 13:52:51 -080052public final class CompanionDeviceManager {
53
Eugene Suslaa38fbf62017-03-14 10:26:10 -070054 private static final boolean DEBUG = false;
Eugene Susla7c3eef22017-03-10 14:25:58 -080055 private static final String LOG_TAG = "CompanionDeviceManager";
56
Eugene Susla6ed45d82017-01-22 13:52:51 -080057 /**
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 /**
Svet Ganovda0acdf2017-02-15 10:28:51 -080064 * 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 /**
Eugene Susla6ed45d82017-01-22 13:52:51 -080072 * 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 */
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 */
90 public abstract void onFailure(CharSequence error);
91 }
92
93 private final ICompanionDeviceManager mService;
94 private final Context mContext;
95
96 /** @hide */
97 public CompanionDeviceManager(
Eugene Susla7c3eef22017-03-10 14:25:58 -080098 @Nullable ICompanionDeviceManager service, @NonNull Context context) {
Eugene Susla6ed45d82017-01-22 13:52:51 -080099 mService = service;
100 mContext = context;
101 }
102
103 /**
104 * Associate this app with a companion device, selected by user
105 *
Svet Ganovda0acdf2017-02-15 10:28:51 -0800106 * <p>Once at least one appropriate device is found, {@code callback} will be called with a
Eugene Susla6ed45d82017-01-22 13:52:51 -0800107 * {@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
Svet Ganovda0acdf2017-02-15 10:28:51 -0800112 * 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
Jeff Sharkey67f9d502017-08-05 13:49:13 -0600116 * you use the {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and {@link
117 * android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} respectively. Note that these
Svet Ganovda0acdf2017-02-15 10:28:51 -0800118 * 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>
Eugene Susla6ed45d82017-01-22 13:52:51 -0800120 *
Eugene Susla47aafbe2017-02-13 12:46:46 -0800121 * <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 *
Eugene Suslab0f97402017-04-24 13:26:12 -0700126 * <p>Calling this API requires a uses-feature
127 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
128 *
Eugene Susla6ed45d82017-01-22 13:52:51 -0800129 * @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 */
136 public void associate(
Eugene Susla36e866b2017-02-23 18:24:39 -0800137 @NonNull AssociationRequest request,
Eugene Susla6ed45d82017-01-22 13:52:51 -0800138 @NonNull Callback callback,
139 @Nullable Handler handler) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800140 if (!checkFeaturePresent()) {
141 return;
142 }
Eugene Suslaa38fbf62017-03-14 10:26:10 -0700143 checkNotNull(request, "Request cannot be null");
144 checkNotNull(callback, "Callback cannot be null");
Eugene Susla6ed45d82017-01-22 13:52:51 -0800145 try {
146 mService.associate(
147 request,
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700148 new CallbackProxy(request, callback, Handler.mainIfNull(handler)),
149 getCallingPackage());
Eugene Susla6ed45d82017-01-22 13:52:51 -0800150 } catch (RemoteException e) {
151 throw e.rethrowFromSystemServer();
152 }
153 }
154
Eugene Susla47aafbe2017-02-13 12:46:46 -0800155 /**
Eugene Suslab0f97402017-04-24 13:26:12 -0700156 * <p>Calling this API requires a uses-feature
157 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
158 *
Eugene Susla47aafbe2017-02-13 12:46:46 -0800159 * @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
163 public List<String> getAssociations() {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800164 if (!checkFeaturePresent()) {
165 return Collections.emptyList();
166 }
Eugene Susla47aafbe2017-02-13 12:46:46 -0800167 try {
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700168 return mService.getAssociations(getCallingPackage(), mContext.getUserId());
Eugene Susla47aafbe2017-02-13 12:46:46 -0800169 } 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 *
Eugene Suslab0f97402017-04-24 13:26:12 -0700183 * <p>Calling this API requires a uses-feature
184 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
185 *
Eugene Susla47aafbe2017-02-13 12:46:46 -0800186 * @param deviceMacAddress the MAC address of device to disassociate from this app
187 */
188 public void disassociate(@NonNull String deviceMacAddress) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800189 if (!checkFeaturePresent()) {
190 return;
191 }
Eugene Susla47aafbe2017-02-13 12:46:46 -0800192 try {
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700193 mService.disassociate(deviceMacAddress, getCallingPackage());
Eugene Susla47aafbe2017-02-13 12:46:46 -0800194 } catch (RemoteException e) {
195 throw e.rethrowFromSystemServer();
196 }
197 }
198
Eugene Suslacf00ade2017-04-10 11:51:58 -0700199 /**
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
Eugene Suslab0f97402017-04-24 13:26:12 -0700208 *
209 * <p>Calling this API requires a uses-feature
210 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
Eugene Suslacf00ade2017-04-10 11:51:58 -0700211 */
212 public void requestNotificationAccess(ComponentName component) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800213 if (!checkFeaturePresent()) {
214 return;
215 }
Eugene Suslacf00ade2017-04-10 11:51:58 -0700216 try {
Eugene Suslad149a082017-06-19 17:27:23 -0700217 IntentSender intentSender = mService.requestNotificationAccess(component)
218 .getIntentSender();
219 mContext.startIntentSender(intentSender, null, 0, 0, 0);
Eugene Suslacf00ade2017-04-10 11:51:58 -0700220 } catch (RemoteException e) {
221 throw e.rethrowFromSystemServer();
Eugene Suslad149a082017-06-19 17:27:23 -0700222 } catch (IntentSender.SendIntentException e) {
Eugene Suslacf00ade2017-04-10 11:51:58 -0700223 throw new RuntimeException(e);
224 }
Eugene Susla6ed45d82017-01-22 13:52:51 -0800225 }
226
Eugene Suslacf00ade2017-04-10 11:51:58 -0700227 /**
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 *
Eugene Suslab0f97402017-04-24 13:26:12 -0700233 * <p>Calling this API requires a uses-feature
234 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
235 *
Eugene Suslacf00ade2017-04-10 11:51:58 -0700236 * @param component the name of the component
237 * @return whether the given component has the notification listener permission
238 */
239 public boolean hasNotificationAccess(ComponentName component) {
Eugene Susla7c3eef22017-03-10 14:25:58 -0800240 if (!checkFeaturePresent()) {
241 return false;
242 }
Eugene Suslacf00ade2017-04-10 11:51:58 -0700243 try {
244 return mService.hasNotificationAccess(component);
245 } catch (RemoteException e) {
246 throw e.rethrowFromSystemServer();
247 }
Eugene Susla6ed45d82017-01-22 13:52:51 -0800248 }
249
Eugene Susla7c3eef22017-03-10 14:25:58 -0800250 private boolean checkFeaturePresent() {
Eugene Suslad7ff1772017-03-24 13:35:45 -0700251 boolean featurePresent = mService != null;
Eugene Susla7c3eef22017-03-10 14:25:58 -0800252 if (!featurePresent && DEBUG) {
253 Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP
254 + " not available");
255 }
256 return featurePresent;
257 }
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700258
259 private Activity getActivity() {
260 return (Activity) mContext;
261 }
262
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
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
282 public void onSuccess(PendingIntent launcher) {
Eugene Susla3a74c7a2017-07-07 14:06:14 -0700283 Handler handler = mHandler;
284 if (handler == null) return;
285 handler.post(() -> {
286 Callback callback = mCallback;
287 if (callback == null) return;
288 callback.onDeviceFound(launcher.getIntentSender());
289 });
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700290 }
291
292 @Override
293 public void onFailure(CharSequence reason) {
Eugene Susla3a74c7a2017-07-07 14:06:14 -0700294 Handler handler = mHandler;
295 if (handler == null) return;
296 handler.post(() -> {
297 Callback callback = mCallback;
298 if (callback == null) return;
299 callback.onFailure(reason);
300 });
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700301 }
302
303 @Override
304 public void onActivityDestroyed(Activity activity) {
Eugene Susla0435e5e2017-06-16 17:20:41 -0700305 if (activity != getActivity()) return;
Eugene Susla6fd0ce32017-04-24 16:13:20 -0700306 try {
307 mService.stopScan(mRequest, this, getCallingPackage());
308 } catch (RemoteException e) {
309 e.rethrowFromSystemServer();
310 }
311 getActivity().getApplication().unregisterActivityLifecycleCallbacks(this);
312 mCallback = null;
313 mHandler = null;
314 mRequest = null;
315 }
316
317 @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
318 @Override public void onActivityStarted(Activity activity) {}
319 @Override public void onActivityResumed(Activity activity) {}
320 @Override public void onActivityPaused(Activity activity) {}
321 @Override public void onActivityStopped(Activity activity) {}
322 @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
323 }
Eugene Susla6ed45d82017-01-22 13:52:51 -0800324}