/* * 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.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityOptions; import android.app.NotificationManager; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.companion.datatransfer.PermissionSyncRequest; 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.Binder; import android.os.Handler; import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.util.ExceptionUtils; import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; import com.android.server.LocalServices; import libcore.io.IoUtils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; 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.BiConsumer; import java.util.function.Consumer; /** * Public interfaces for managing companion devices. * *
The interfaces in this class allow companion apps to * {@link #associate(AssociationRequest, Executor, Callback)} discover and request device profiles} * for companion devices, {@link #startObservingDevicePresence(String) listen to device presence * events}, {@link #startSystemDataTransfer(int, Executor, OutcomeReceiver) transfer system level * data} via {@link #attachSystemDataTransport(int, InputStream, OutputStream) the reported * channel} and more.
* *For more information about managing companion devices, read the Companion Device Pairing * developer guide. *
* 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 final 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 (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); 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
* *Note that if you use this api to associate with a Bluetooth device, please make sure * to cancel your own Bluetooth discovery before calling this api, otherwise the callback * may fail to return the desired device.
* *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) * @see BluetoothAdapter#cancelDiscovery() */ @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 (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); 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(); } } /** * Cancel the current association activity. * *The app should launch the returned {@code intentSender} by calling * {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int)} to * cancel the current association activity
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @return An {@link IntentSender} that the app should use to launch in order to cancel the * current association activity */ @UserHandleAware @Nullable public IntentSender buildAssociationCancellationIntent() { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return null; } try { PendingIntent pendingIntent = mService.buildAssociationCancellationIntent( mContext.getOpPackageName(), mContext.getUserId()); return pendingIntent.getIntentSender(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** *Enable system data sync (it only supports call metadata sync for now). * By default all supported system data types are enabled.
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @param associationId id of the device association. * @param flags system data types to be enabled. */ public void enableSystemDataSyncForTypes(int associationId, @DataSyncTypes int flags) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { mService.enableSystemDataSync(associationId, flags); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** *Disable system data sync (it only supports call metadata sync for now). * By default all supported system data types are enabled.
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @param associationId id of the device association. * @param flags system data types to be disabled. */ public void disableSystemDataSyncForTypes(int associationId, @DataSyncTypes int flags) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { mService.disableSystemDataSync(associationId, flags); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * @hide */ public void enablePermissionsSync(int associationId) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { mService.enablePermissionsSync(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * @hide */ public void disablePermissionsSync(int associationId) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { mService.disablePermissionsSync(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * @hide */ public PermissionSyncRequest getPermissionSyncRequest(int associationId) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return null; } try { return mService.getPermissionSyncRequest(associationId); } 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. Device * address is case-sensitive in API level < 33. * * @deprecated use {@link #disassociate(int)} */ @UserHandleAware @Deprecated public void disassociate(@NonNull String deviceMacAddress) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); 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 (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); 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. * * Side-loaded apps must allow restricted settings before requesting notification access. * *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
*/ @UserHandleAware public void requestNotificationAccess(ComponentName component) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { PendingIntent pendingIntent = mService.requestNotificationAccess( component, mContext.getUserId()); if (pendingIntent == null) { return; } IntentSender intentSender = pendingIntent.getIntentSender(); mContext.startIntentSender(intentSender, null, 0, 0, 0, ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle()); } 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 (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); 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 (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); 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) @NonNull public ListThis is an asynchronous call, it will return immediately. Register for {@link * BluetoothDevice#ACTION_BOND_STATE_CHANGED} intents to be notified when the bond removal * process completes, and its result. * * @param associationId an already-associated companion device to remove bond from * @return false on immediate error, true if bond removal process will begin */ @FlaggedApi(Flags.FLAG_UNPAIR_ASSOCIATED_DEVICE) @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean removeBond(int associationId) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return false; } try { return mService.removeBond(associationId, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Register to receive callbacks whenever the associated device comes in and out of range. * *
The provided device must be {@link #associate associated} with the calling app before * calling this method.
* *Caller must implement a single {@link CompanionDeviceService} which will be bound to and * receive callbacks to {@link CompanionDeviceService#onDeviceAppeared} and * {@link CompanionDeviceService#onDeviceDisappeared}. * The app doesn't need to remain running in order to receive its callbacks.
* *Calling app must declare uses-permission * {@link android.Manifest.permission#REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE}.
* *Calling app must check for feature presence of * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.
* *For Bluetooth LE devices, this is based on scanning for device with the given address. * The system will scan for the device when Bluetooth is ON or Bluetooth scanning is ON.
* *For Bluetooth classic devices this is triggered when the device connects/disconnects. * WiFi devices are not supported.
* *If a Bluetooth LE device wants to use a rotating mac address, it is recommended to use * Resolvable Private Address, and ensure the device is bonded to the phone so that android OS * is able to resolve the address.
* * @param deviceAddress a previously-associated companion device's address * * @throws DeviceNotAssociatedException if the given device was not previously associated * with this app. */ @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String deviceAddress) throws DeviceNotAssociatedException { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { mService.legacyStartObservingDevicePresence(deviceAddress, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); throw e.rethrowFromSystemServer(); } int callingUid = Binder.getCallingUid(); int callingPid = Binder.getCallingPid(); ActivityManagerInternal managerInternal = LocalServices.getService(ActivityManagerInternal.class); if (managerInternal != null) { managerInternal .logFgsApiBegin(ActivityManager.FOREGROUND_SERVICE_API_TYPE_CDM, callingUid, callingPid); } } // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Unregister for receiving callbacks whenever the associated device comes in and out of range. * * The provided device must be {@link #associate associated} with the calling app before * calling this method. * * Calling app must declare uses-permission * {@link android.Manifest.permission#REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE}. * * Calling app must check for feature presence of * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API. * * @param deviceAddress a previously-associated companion device's address * * @throws DeviceNotAssociatedException if the given device was not previously associated * with this app. */ @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String deviceAddress) throws DeviceNotAssociatedException { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { mService.legacyStopObservingDevicePresence(deviceAddress, mContext.getPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); } int callingUid = Binder.getCallingUid(); int callingPid = Binder.getCallingPid(); ActivityManagerInternal managerInternal = LocalServices.getService(ActivityManagerInternal.class); if (managerInternal != null) { managerInternal .logFgsApiEnd(ActivityManager.FOREGROUND_SERVICE_API_TYPE_CDM, callingUid, callingPid); } } /** * Register to receive callbacks whenever the associated device comes in and out of range. * *The app doesn't need to remain running in order to receive its callbacks.
* *Calling app must check for feature presence of * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.
* *For Bluetooth LE devices, this is based on scanning for device with the given address. * The system will scan for the device when Bluetooth is ON or Bluetooth scanning is ON.
* *For Bluetooth classic devices this is triggered when the device connects/disconnects.
* *WiFi devices are not supported.
* *If a Bluetooth LE device wants to use a rotating mac address, it is recommended to use * Resolvable Private Address, and ensure the device is bonded to the phone so that android OS * is able to resolve the address.
* * @param request A request for setting the types of device for observing device presence. * * @see ObservingDevicePresenceRequest.Builder * @see CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent) */ @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } Objects.requireNonNull(request, "request cannot be null"); try { mService.startObservingDevicePresence( request, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Unregister for receiving callbacks whenever the associated device comes in and out of range. * * Calling app must check for feature presence of * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API. * * @param request A request for setting the types of device for observing device presence. */ @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } Objects.requireNonNull(request, "request cannot be null"); try { mService.stopObservingDevicePresence( request, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Dispatch a message to system for processing. It should only be called by * {@link CompanionDeviceService#dispatchMessageToSystem(int, int, byte[])} * *Calling 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 */ @Deprecated @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public void dispatchMessage(int messageId, int associationId, @NonNull byte[] message) throws DeviceNotAssociatedException { Log.w(TAG, "dispatchMessage replaced by attachSystemDataTransport"); } /** * Attach a bidirectional communication stream to be used as a transport channel for * transporting system data between associated devices. * * @param associationId id of the associated device. * @param in Already connected stream of data incoming from remote * associated device. * @param out Already connected stream of data outgoing to remote associated * device. * @throws DeviceNotAssociatedException Thrown if the associationId was not previously * associated with this app. * * @see #buildPermissionTransferUserConsentIntent(int) * @see #startSystemDataTransfer(int, Executor, OutcomeReceiver) * @see #detachSystemDataTransport(int) */ @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public void attachSystemDataTransport(int associationId, @NonNull InputStream in, @NonNull OutputStream out) throws DeviceNotAssociatedException { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } synchronized (mTransports) { if (mTransports.contains(associationId)) { detachSystemDataTransport(associationId); } try { final Transport transport = new Transport(associationId, in, out); mTransports.put(associationId, transport); transport.start(); } catch (IOException e) { throw new RuntimeException("Failed to attach transport", e); } } } /** * Detach the transport channel that's previously attached for the associated device. The system * will stop transferring any system data when this method is called. * * @param associationId id of the associated device. * @throws DeviceNotAssociatedException Thrown if the associationId was not previously * associated with this app. * * @see #attachSystemDataTransport(int, InputStream, OutputStream) */ @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public void detachSystemDataTransport(int associationId) throws DeviceNotAssociatedException { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } synchronized (mTransports) { final Transport transport = mTransports.get(associationId); if (transport != null) { mTransports.delete(associationId); transport.stop(); } } } /** * 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 (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); 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) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { mService.notifySelfManagedDeviceAppeared(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) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { mService.notifySelfManagedDeviceDisappeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Build a permission sync user consent dialog. * *Only the companion app which owns the association can call this method. Otherwise a null * IntentSender will be returned from this method and an error will be logged. * The app should launch the {@link Activity} in the returned {@code intentSender} * {@link IntentSender} by calling * {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}.
* *The permission transfer doesn't happen immediately after the call or when the user * consents. The app needs to call * {@link #attachSystemDataTransport(int, InputStream, OutputStream)} to attach a transport * channel and * {@link #startSystemDataTransfer(int, Executor, OutcomeReceiver)} to trigger the system data * transfer}.
* * @param associationId The unique {@link AssociationInfo#getId ID} assigned to the association * of the companion device recorded by CompanionDeviceManager * @return An {@link IntentSender} that the app should use to launch the UI for * the user to confirm the system data transfer request. * * @see #attachSystemDataTransport(int, InputStream, OutputStream) * @see #startSystemDataTransfer(int, Executor, OutcomeReceiver) */ @UserHandleAware @Nullable public IntentSender buildPermissionTransferUserConsentIntent(int associationId) throws DeviceNotAssociatedException { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return null; } try { PendingIntent pendingIntent = mService.buildPermissionTransferUserConsentIntent( mContext.getOpPackageName(), mContext.getUserId(), associationId); if (pendingIntent == null) { return null; } return pendingIntent.getIntentSender(); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); throw e.rethrowFromSystemServer(); } } /** * Return the current state of consent for permission transfer for the association. * True if the user has allowed permission transfer for the association, false otherwise. * ** Note: The initial user consent is collected via * {@link #buildPermissionTransferUserConsentIntent(int) a permission transfer user consent dialog}. * After the user has made their initial selection, they can toggle the permission transfer * feature in the settings. * This method always returns the state of the toggle setting. *
* * @param associationId The unique {@link AssociationInfo#getId ID} assigned to the association * of the companion device recorded by CompanionDeviceManager * @return True if the user has consented to the permission transfer, or false otherwise. * @throws DeviceNotAssociatedException Exception if the companion device is not associated with * the user or the calling app. */ @UserHandleAware @FlaggedApi(Flags.FLAG_PERM_SYNC_USER_CONSENT) public boolean isPermissionTransferUserConsented(int associationId) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return false; } try { return mService.isPermissionTransferUserConsented(mContext.getOpPackageName(), mContext.getUserId(), associationId); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); throw e.rethrowFromSystemServer(); } } /** * Start system data transfer which has been previously approved by the user. * *Before calling this method, the app needs to make sure there's a communication channel * between two devices, and has prompted user consent dialogs built by one of these methods: * {@link #buildPermissionTransferUserConsentIntent(int)}. * The transfer may fail if the communication channel is disconnected during the transfer.
* * @param associationId The unique {@link AssociationInfo#getId ID} assigned to the Association * of the companion device recorded by CompanionDeviceManager * @throws DeviceNotAssociatedException Exception if the companion device is not associated * @deprecated Use {@link #startSystemDataTransfer(int, Executor, OutcomeReceiver)} instead. * @hide */ @Deprecated @UserHandleAware public void startSystemDataTransfer(int associationId) throws DeviceNotAssociatedException { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } try { mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(), associationId, null); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); throw e.rethrowFromSystemServer(); } } /** * Start system data transfer which has been previously approved by the user. * *Before calling this method, the app needs to make sure * {@link #attachSystemDataTransport(int, InputStream, OutputStream) the transport channel is * attached}, and * {@link #buildPermissionTransferUserConsentIntent(int) the user consent dialog has prompted to * the user}. * The transfer will fail if the transport channel is disconnected or * {@link #detachSystemDataTransport(int) detached} during the transfer.
* * @param associationId The unique {@link AssociationInfo#getId ID} assigned to the Association * of the companion device recorded by CompanionDeviceManager * @param executor The executor which will be used to invoke the result callback. * @param result The callback to notify the app of the result of the system data transfer. * @throws DeviceNotAssociatedException Exception if the companion device is not associated */ @UserHandleAware public void startSystemDataTransfer( int associationId, @NonNull Executor executor, @NonNull OutcomeReceiverThe length of the tag must be at most 1024 characters to save disk space. * *
This allows to store useful information about the associated devices. * * @param associationId The unique {@link AssociationInfo#getId ID} assigned to the Association * of the companion device recorded by CompanionDeviceManager * @param tag the tag of this association */ @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @UserHandleAware public void setAssociationTag(int associationId, @NonNull String tag) { if (mService == null) { Log.w(TAG, "CompanionDeviceManager service is not available."); return; } Objects.requireNonNull(tag, "tag cannot be null"); if (tag.length() > ASSOCIATION_TAG_LENGTH_LIMIT) { throw new IllegalArgumentException("Length of the tag must be at most" + ASSOCIATION_TAG_LENGTH_LIMIT + " characters"); } try { mService.setAssociationTag(associationId, tag); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Clears the {@link AssociationInfo#getTag() tag} for this association. * *
The tag will be set to null for this association when calling this API.
*
* @param associationId The unique {@link AssociationInfo#getId ID} assigned to the Association
* of the companion device recorded by CompanionDeviceManager
* @see CompanionDeviceManager#setAssociationTag(int, String)
*/
@FlaggedApi(Flags.FLAG_ASSOCIATION_TAG)
@UserHandleAware
public void clearAssociationTag(int associationId) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
try {
mService.clearAssociationTag(associationId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
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
* Internally uses two threads to shuttle bidirectional data between a
* remote device and a {@code socketpair} that the system is listening to.
* This design ensures that data payloads are transported efficiently
* without adding Binder traffic contention.
*/
private class Transport {
private final int mAssociationId;
private final InputStream mRemoteIn;
private final OutputStream mRemoteOut;
private InputStream mLocalIn;
private OutputStream mLocalOut;
private volatile boolean mStopped;
public Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) {
mAssociationId = associationId;
mRemoteIn = remoteIn;
mRemoteOut = remoteOut;
}
public void start() throws IOException {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
final ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair();
final ParcelFileDescriptor localFd = pair[0];
final ParcelFileDescriptor remoteFd = pair[1];
mLocalIn = new ParcelFileDescriptor.AutoCloseInputStream(localFd);
mLocalOut = new ParcelFileDescriptor.AutoCloseOutputStream(localFd);
try {
mService.attachSystemDataTransport(mContext.getOpPackageName(),
mContext.getUserId(), mAssociationId, remoteFd);
} catch (RemoteException e) {
throw new IOException("Failed to configure transport", e);
}
new Thread(() -> {
try {
copyWithFlushing(mLocalIn, mRemoteOut);
} catch (IOException e) {
if (!mStopped) {
Log.w(TAG, "Trouble during outgoing transport", e);
stop();
}
}
}).start();
new Thread(() -> {
try {
copyWithFlushing(mRemoteIn, mLocalOut);
} catch (IOException e) {
if (!mStopped) {
Log.w(TAG, "Trouble during incoming transport", e);
stop();
}
}
}).start();
}
public void stop() {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
mStopped = true;
try {
mService.detachSystemDataTransport(mContext.getOpPackageName(),
mContext.getUserId(), mAssociationId);
} catch (RemoteException | IllegalArgumentException e) {
Log.w(TAG, "Failed to detach transport", e);
}
IoUtils.closeQuietly(mRemoteIn);
IoUtils.closeQuietly(mRemoteOut);
IoUtils.closeQuietly(mLocalIn);
IoUtils.closeQuietly(mLocalOut);
}
/**
* Copy all data from the first stream to the second stream, flushing
* after every write to ensure that we quickly deliver all pending data.
*/
private void copyWithFlushing(@NonNull InputStream in, @NonNull OutputStream out)
throws IOException {
byte[] buffer = new byte[8192];
int c;
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
out.flush();
}
}
}
}
> mListener;
private OnTransportsChangedListenerProxy(Executor executor,
Consumer
> listener) {
mExecutor = executor;
mListener = listener;
}
@Override
public void onTransportsChanged(@NonNull List