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 import java.util.function.BiConsumer; 41 42 /** 43 * System level service for managing companion devices 44 * 45 * <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> 49 * 50 * @see AssociationRequest 51 */ 52 @SystemService(Context.COMPANION_DEVICE_SERVICE) 53 public final class CompanionDeviceManager { 54 55 private static final boolean DEBUG = false; 56 private static final String LOG_TAG = "CompanionDeviceManager"; 57 58 /** 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 /** 65 * 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 /** 73 * 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 */ onDeviceFound(IntentSender chooserLauncher)84 public abstract void onDeviceFound(IntentSender chooserLauncher); 85 86 /** 87 * Called if there was an error looking for device(s) 88 * 89 * @param error the cause of the error 90 */ onFailure(CharSequence error)91 public abstract void onFailure(CharSequence error); 92 } 93 94 private final ICompanionDeviceManager mService; 95 private final Context mContext; 96 97 /** @hide */ CompanionDeviceManager( @ullable ICompanionDeviceManager service, @NonNull Context context)98 public CompanionDeviceManager( 99 @Nullable ICompanionDeviceManager service, @NonNull Context context) { 100 mService = service; 101 mContext = context; 102 } 103 104 /** 105 * Associate this app with a companion device, selected by user 106 * 107 * <p>Once at least one appropriate device is found, {@code callback} will be called with a 108 * {@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 113 * 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 117 * 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 119 * 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> 121 * 122 * <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 * 127 * <p>Calling this API requires a uses-feature 128 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 129 * 130 * @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 */ associate( @onNull AssociationRequest request, @NonNull Callback callback, @Nullable Handler handler)137 public void associate( 138 @NonNull AssociationRequest request, 139 @NonNull Callback callback, 140 @Nullable Handler handler) { 141 if (!checkFeaturePresent()) { 142 return; 143 } 144 checkNotNull(request, "Request cannot be null"); 145 checkNotNull(callback, "Callback cannot be null"); 146 try { 147 mService.associate( 148 request, 149 new CallbackProxy(request, callback, Handler.mainIfNull(handler)), 150 getCallingPackage()); 151 } catch (RemoteException e) { 152 throw e.rethrowFromSystemServer(); 153 } 154 } 155 156 /** 157 * <p>Calling this API requires a uses-feature 158 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 159 * 160 * @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 getAssociations()164 public List<String> getAssociations() { 165 if (!checkFeaturePresent()) { 166 return Collections.emptyList(); 167 } 168 try { 169 return mService.getAssociations(getCallingPackage(), mContext.getUserId()); 170 } 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 * 184 * <p>Calling this API requires a uses-feature 185 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 186 * 187 * @param deviceMacAddress the MAC address of device to disassociate from this app 188 */ disassociate(@onNull String deviceMacAddress)189 public void disassociate(@NonNull String deviceMacAddress) { 190 if (!checkFeaturePresent()) { 191 return; 192 } 193 try { 194 mService.disassociate(deviceMacAddress, getCallingPackage()); 195 } catch (RemoteException e) { 196 throw e.rethrowFromSystemServer(); 197 } 198 } 199 200 /** 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 209 * 210 * <p>Calling this API requires a uses-feature 211 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 212 */ requestNotificationAccess(ComponentName component)213 public void requestNotificationAccess(ComponentName component) { 214 if (!checkFeaturePresent()) { 215 return; 216 } 217 try { 218 IntentSender intentSender = mService.requestNotificationAccess(component) 219 .getIntentSender(); 220 mContext.startIntentSender(intentSender, null, 0, 0, 0); 221 } catch (RemoteException e) { 222 throw e.rethrowFromSystemServer(); 223 } catch (IntentSender.SendIntentException e) { 224 throw new RuntimeException(e); 225 } 226 } 227 228 /** 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 * 234 * <p>Calling this API requires a uses-feature 235 * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> 236 * 237 * @param component the name of the component 238 * @return whether the given component has the notification listener permission 239 */ hasNotificationAccess(ComponentName component)240 public boolean hasNotificationAccess(ComponentName component) { 241 if (!checkFeaturePresent()) { 242 return false; 243 } 244 try { 245 return mService.hasNotificationAccess(component); 246 } catch (RemoteException e) { 247 throw e.rethrowFromSystemServer(); 248 } 249 } 250 checkFeaturePresent()251 private boolean checkFeaturePresent() { 252 boolean featurePresent = mService != null; 253 if (!featurePresent && DEBUG) { 254 Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP 255 + " not available"); 256 } 257 return featurePresent; 258 } 259 getActivity()260 private Activity getActivity() { 261 return (Activity) mContext; 262 } 263 getCallingPackage()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 275 final Object mLock = new Object(); 276 CallbackProxy(AssociationRequest request, Callback callback, Handler handler)277 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 onSuccess(PendingIntent launcher)285 public void onSuccess(PendingIntent launcher) { 286 lockAndPost(Callback::onDeviceFound, launcher.getIntentSender()); 287 } 288 289 @Override onFailure(CharSequence reason)290 public void onFailure(CharSequence reason) { 291 lockAndPost(Callback::onFailure, reason); 292 } 293 lockAndPost(BiConsumer<Callback, T> action, T payload)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 } 308 } 309 310 @Override onActivityDestroyed(Activity activity)311 public void onActivityDestroyed(Activity activity) { 312 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; 323 } 324 } 325 onActivityCreated(Activity activity, Bundle savedInstanceState)326 @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} onActivityStarted(Activity activity)327 @Override public void onActivityStarted(Activity activity) {} onActivityResumed(Activity activity)328 @Override public void onActivityResumed(Activity activity) {} onActivityPaused(Activity activity)329 @Override public void onActivityPaused(Activity activity) {} onActivityStopped(Activity activity)330 @Override public void onActivityStopped(Activity activity) {} onActivitySaveInstanceState(Activity activity, Bundle outState)331 @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} 332 } 333 } 334