/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.companion; import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING; import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION; import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER; import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UserHandleAware; import android.app.Activity; import android.app.NotificationManager; import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager; import android.net.MacAddress; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.util.ExceptionUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * System level service for managing companion devices * * See this guide * for a usage example. * *
To obtain an instance call {@link Context#getSystemService}({@link * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest, * Callback, Handler)} to initiate the flow of associating current package with a * device selected by user.
* * @see CompanionDeviceManager#associate * @see AssociationRequest */ @SystemService(Context.COMPANION_DEVICE_SERVICE) public final class CompanionDeviceManager { private static final boolean DEBUG = false; private static final String LOG_TAG = "CompanionDeviceManager"; /** * The result code to propagate back to the originating activity, indicates the association * dialog is explicitly declined by the users. * * @hide */ public static final int RESULT_USER_REJECTED = 1; /** * The result code to propagate back to the originating activity, indicates the association * dialog is dismissed if there's no device found after 20 seconds. * * @hide */ public static final int RESULT_DISCOVERY_TIMEOUT = 2; /** * The result code to propagate back to the originating activity, indicates the internal error * in CompanionDeviceManager. * * @hide */ public static final int RESULT_INTERNAL_ERROR = 3; /** * Requesting applications will receive the String in {@link Callback#onFailure} if the * association dialog is explicitly declined by the users. e.g. press the Don't allow button. * * @hide */ public static final String REASON_USER_REJECTED = "user_rejected"; /** * Requesting applications will receive the String in {@link Callback#onFailure} if there's * no device found after 20 seconds. * * @hide */ public static final String REASON_DISCOVERY_TIMEOUT = "discovery_timeout"; /** * Requesting applications will receive the String in {@link Callback#onFailure} if the * association dialog is in-explicitly declined by the users. e.g. phone is locked, switch to * another app or press outside the dialog. * * @hide */ public static final String REASON_CANCELED = "canceled"; /** * A device, returned in the activity result of the {@link IntentSender} received in * {@link Callback#onDeviceFound} * * Type is: ** The {@link Callback#onAssociationPending(IntentSender)} is invoked after the * {@link AssociationRequest} has been checked by the Companion Device Manager Service and is * pending user's approval. * * The {@link IntentSender} received as an argument to * {@link Callback#onAssociationPending(IntentSender)} "encapsulates" an {@link Activity} * that has UI for the user to: *
* Upon receiving user's confirmation Companion Device Manager Service will create an * association and will send an {@link AssociationInfo} object that represents the created * association back to the application both via * {@link Callback#onAssociationCreated(AssociationInfo)} and * via {@link Activity#setResult(int, Intent)}. * In the latter the {@code resultCode} will be set to {@link Activity#RESULT_OK} and the * {@code data} {@link Intent} will contain {@link AssociationInfo} extra named * {@link #EXTRA_ASSOCIATION}. *
*
* if (resultCode == Activity.RESULT_OK) {
* AssociationInfo associationInfo = data.getParcelableExtra(EXTRA_ASSOCIATION);
* }
*
*
*
*
* * If the Companion Device Manager Service is not able to create an association, it will * invoke {@link Callback#onFailure(CharSequence)}. * * If this happened after the application has launched the UI (eg. the user chose to reject * the association), the outcome will also be delivered to the applications via * {@link Activity#setResult(int)} with the {@link Activity#RESULT_CANCELED} * {@code resultCode}. *
* ** Note that in some cases the Companion Device Manager Service may not need to collect * user's approval for creating an association. In such cases, this method will not be * invoked, and {@link #onAssociationCreated(AssociationInfo)} may be invoked right away. *
* * @see #associate(AssociationRequest, Executor, Callback) * @see #associate(AssociationRequest, Callback, Handler) * @see #EXTRA_ASSOCIATION */ public abstract static class Callback { /** * @deprecated method was renamed to onAssociationPending() to provide better clarity; both * methods are functionally equivalent and only one needs to be overridden. * * @see #onAssociationPending(IntentSender) */ @Deprecated public void onDeviceFound(@NonNull IntentSender intentSender) {} /** * Invoked when the association needs to approved by the user. * * Applications should launch the {@link Activity} "encapsulated" in {@code intentSender} * {@link IntentSender} object by calling * {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}. * * @param intentSender an {@link IntentSender} which applications should use to launch * the UI for the user to confirm the association. */ public void onAssociationPending(@NonNull IntentSender intentSender) { onDeviceFound(intentSender); } /** * Invoked when the association is created. * * @param associationInfo contains details of the newly-established association. */ public void onAssociationCreated(@NonNull AssociationInfo associationInfo) {} /** * Invoked if the association could not be created. * * @param error error message. */ public abstract void onFailure(@Nullable CharSequence error); } private final ICompanionDeviceManager mService; private Context mContext; @GuardedBy("mListeners") private final ArrayListNote that before creating establishing association the system may need to show UI to * collect user confirmation.
* *If the app needs to be excluded from battery optimizations (run in the background) * or to have unrestricted data access (use data in the background) it should declare use of * {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and * {@link android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} in its * AndroidManifest.xml respectively. * Note that these special capabilities have a negative effect on the device's battery and * user's data usage, therefore you should request them when absolutely necessary.
* *Application can use {@link #getMyAssociations()} for retrieving the list of currently * {@link AssociationInfo} objects, that represent their existing associations. * Applications can also use {@link #disassociate(int)} to remove an association, and are * recommended to do when an association is no longer relevant to avoid unnecessary battery * and/or data drain resulting from special privileges that the association provides
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
** * @param request A request object that describes details of the request. * @param callback The callback used to notify application when the association is created. * @param handler The handler which will be used to invoke the callback. * * @see AssociationRequest.Builder * @see #getMyAssociations() * @see #disassociate(int) * @see #associate(AssociationRequest, Executor, Callback) */ @UserHandleAware @RequiresPermission(anyOf = { REQUEST_COMPANION_PROFILE_WATCH, REQUEST_COMPANION_PROFILE_COMPUTER, REQUEST_COMPANION_PROFILE_APP_STREAMING, REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION, }, conditional = true) public void associate( @NonNull AssociationRequest request, @NonNull Callback callback, @Nullable Handler handler) { if (!checkFeaturePresent()) return; Objects.requireNonNull(request, "Request cannot be null"); Objects.requireNonNull(callback, "Callback cannot be null"); handler = Handler.mainIfNull(handler); try { mService.associate(request, new AssociationRequestCallbackProxy(handler, callback), mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Request to associate this app with a companion device. * *Note that before creating establishing association the system may need to show UI to * collect user confirmation.
* *If the app needs to be excluded from battery optimizations (run in the background) * or to have unrestricted data access (use data in the background) it should declare use of * {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and * {@link android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} in its * AndroidManifest.xml respectively. * Note that these special capabilities have a negative effect on the device's battery and * user's data usage, therefore you should request them when absolutely necessary.
* *Application can use {@link #getMyAssociations()} for retrieving the list of currently * {@link AssociationInfo} objects, that represent their existing associations. * Applications can also use {@link #disassociate(int)} to remove an association, and are * recommended to do when an association is no longer relevant to avoid unnecessary battery * and/or data drain resulting from special privileges that the association provides
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
** * @param request A request object that describes details of the request. * @param executor The executor which will be used to invoke the callback. * @param callback The callback used to notify application when the association is created. * * @see AssociationRequest.Builder * @see #getMyAssociations() * @see #disassociate(int) */ @UserHandleAware @RequiresPermission(anyOf = { REQUEST_COMPANION_PROFILE_WATCH, REQUEST_COMPANION_PROFILE_COMPUTER, REQUEST_COMPANION_PROFILE_APP_STREAMING, REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION }, conditional = true) public void associate( @NonNull AssociationRequest request, @NonNull Executor executor, @NonNull Callback callback) { if (!checkFeaturePresent()) return; Objects.requireNonNull(request, "Request cannot be null"); Objects.requireNonNull(executor, "Executor cannot be null"); Objects.requireNonNull(callback, "Callback cannot be null"); try { mService.associate(request, new AssociationRequestCallbackProxy(executor, callback), mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @return a list of MAC addresses of devices that have been previously associated with the * current app are managed by CompanionDeviceManager (ie. does not include devices managed by * application itself even if they have a MAC address). * * @deprecated use {@link #getMyAssociations()} */ @Deprecated @UserHandleAware @NonNull public ListCalling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @return a list of associations that have been previously associated with the current app. */ @UserHandleAware @NonNull public ListAny privileges provided via being associated with a given device will be revoked
* *Consider doing so when the * association is no longer relevant to avoid unnecessary battery and/or data drain resulting * from special privileges that the association provides
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @param deviceMacAddress the MAC address of device to disassociate from this app * * @deprecated use {@link #disassociate(int)} */ @UserHandleAware @Deprecated public void disassociate(@NonNull String deviceMacAddress) { if (!checkFeaturePresent()) return; try { mService.legacyDisassociate(deviceMacAddress, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Remove an association. * *Any privileges provided via being associated with a given device will be revoked
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @param associationId id of the association to be removed. * * @see #associate(AssociationRequest, Executor, Callback) * @see AssociationInfo#getId() */ @UserHandleAware public void disassociate(int associationId) { if (!checkFeaturePresent()) return; try { mService.disassociate(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Request notification access for the given component. * * The given component must follow the protocol specified in {@link NotificationListenerService} * * Only components from the same {@link ComponentName#getPackageName package} as the calling app * are allowed. * * Your app must have an association with a device before calling this API * *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
*/ @UserHandleAware public void requestNotificationAccess(ComponentName component) { if (!checkFeaturePresent()) { return; } try { IntentSender intentSender = mService .requestNotificationAccess(component, mContext.getUserId()) .getIntentSender(); mContext.startIntentSender(intentSender, null, 0, 0, 0); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (IntentSender.SendIntentException e) { throw new RuntimeException(e); } } /** * Check whether the given component can access the notifications via a * {@link NotificationListenerService} * * Your app must have an association with a device before calling this API * *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @param component the name of the component * @return whether the given component has the notification listener permission * * @deprecated Use * {@link NotificationManager#isNotificationListenerAccessGranted(ComponentName)} instead. */ @Deprecated public boolean hasNotificationAccess(ComponentName component) { if (!checkFeaturePresent()) { return false; } try { return mService.hasNotificationAccess(component); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Check if a given package was {@link #associate associated} with a device with given * Wi-Fi MAC address for a given user. * *This is a system API protected by the * {@link android.Manifest.permission#MANAGE_COMPANION_DEVICES} permission, that’s currently * called by the Android Wi-Fi stack to determine whether user consent is required to connect * to a Wi-Fi network. Devices that have been pre-registered as companion devices will not * require user consent to connect.
* *Note if the caller has the * {@link android.Manifest.permission#COMPANION_APPROVE_WIFI_CONNECTIONS} permission, this * method will return true by default.
* * @param packageName the name of the package that has the association with the companion device * @param macAddress the Wi-Fi MAC address or BSSID of the companion device to check for * @param user the user handle that currently hosts the package being queried for a companion * device association * @return whether a corresponding association record exists * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public boolean isDeviceAssociatedForWifiConnection( @NonNull String packageName, @NonNull MacAddress macAddress, @NonNull UserHandle user) { if (!checkFeaturePresent()) return false; Objects.requireNonNull(packageName, "package name cannot be null"); Objects.requireNonNull(macAddress, "mac address cannot be null"); Objects.requireNonNull(user, "user cannot be null"); try { return mService.isDeviceAssociatedForWifiConnection( packageName, macAddress.toString(), user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Gets all package-device {@link AssociationInfo}s for the current user. * * @return the associations list * @see #addOnAssociationsChangedListener(Executor, OnAssociationsChangedListener) * @see #removeOnAssociationsChangedListener(OnAssociationsChangedListener) * @hide */ @SystemApi @UserHandleAware @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public @NonNull ListCalling app must declare uses-permission * {@link android.Manifest.permission#DELIVER_COMPANION_MESSAGES}
* * @param messageId id of the message * @param associationId association id of the associated device where data is coming from * @param message message received from the associated device * * @throws DeviceNotAssociatedException if the given device was not previously associated with * this app * * @hide */ @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public void dispatchMessage(int messageId, int associationId, @NonNull byte[] message) throws DeviceNotAssociatedException { try { mService.dispatchMessage(messageId, associationId, message); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); throw e.rethrowFromSystemServer(); } } /** * Associates given device with given app for the given user directly, without UI prompt. * * @param packageName package name of the companion app * @param macAddress mac address of the device to associate * @param certificate The SHA256 digest of the companion app's signing certificate * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES) public void associate( @NonNull String packageName, @NonNull MacAddress macAddress, @NonNull byte[] certificate) { if (!checkFeaturePresent()) { return; } Objects.requireNonNull(packageName, "package name cannot be null"); Objects.requireNonNull(macAddress, "mac address cannot be null"); UserHandle user = android.os.Process.myUserHandle(); try { mService.createAssociation( packageName, macAddress.toString(), user.getIdentifier(), certificate); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Notify the system that the given self-managed association has just appeared. * This causes the system to bind to the companion app to keep it running until the association * is reported as disappeared * *This API is only available for the companion apps that manage the connectivity by * themselves.
* * @param associationId the unique {@link AssociationInfo#getId ID} assigned to the Association * recorded by CompanionDeviceManager * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceAppeared(int associationId) { try { mService.notifyDeviceAppeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Notify the system that the given self-managed association has just disappeared. * This causes the system to unbind to the companion app. * *This API is only available for the companion apps that manage the connectivity by * themselves.
* * @param associationId the unique {@link AssociationInfo#getId ID} assigned to the Association * recorded by CompanionDeviceManager * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceDisappeared(int associationId) { try { mService.notifyDeviceDisappeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private boolean checkFeaturePresent() { boolean featurePresent = mService != null; if (!featurePresent && DEBUG) { Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP + " not available"); } return featurePresent; } private static class AssociationRequestCallbackProxy extends IAssociationRequestCallback.Stub { private final Handler mHandler; private final Callback mCallback; private final Executor mExecutor; private AssociationRequestCallbackProxy( @NonNull Executor executor, @NonNull Callback callback) { mExecutor = executor; mHandler = null; mCallback = callback; } private AssociationRequestCallbackProxy( @NonNull Handler handler, @NonNull Callback callback) { mHandler = handler; mExecutor = null; mCallback = callback; } @Override public void onAssociationPending(@NonNull PendingIntent pi) { execute(mCallback::onAssociationPending, pi.getIntentSender()); } @Override public void onAssociationCreated(@NonNull AssociationInfo association) { execute(mCallback::onAssociationCreated, association); } @Override public void onFailure(CharSequence error) throws RemoteException { execute(mCallback::onFailure, error); } private