script-astra/Android/Sdk/sources/android-35/android/companion/CompanionDeviceManager.java
localadmin 4380f00a78 init
2025-01-20 18:15:20 +03:00

1966 lines
76 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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.
*
* <p>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.</p>
*
* <div class="special reference">
* <h3>Developer Guides</h3>
* <p>For more information about managing companion devices, read the <a href=
* "{@docRoot}guide/topics/connectivity/companion-device-pairing">Companion Device Pairing</a>
* developer guide.
* </div>
*/
@SuppressLint("LongLogTag")
@SystemService(Context.COMPANION_DEVICE_SERVICE)
@RequiresFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
public final class CompanionDeviceManager {
private static final String TAG = "CDM_CompanionDeviceManager";
/** @hide */
@IntDef(prefix = {"RESULT_"}, value = {
RESULT_OK,
RESULT_CANCELED,
RESULT_USER_REJECTED,
RESULT_DISCOVERY_TIMEOUT,
RESULT_INTERNAL_ERROR
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResultCode {}
/**
* The result code to propagate back to the user activity, indicates the association
* is created successfully.
*/
public static final int RESULT_OK = -1;
/**
* The result code to propagate back to the user activity, indicates if the association dialog
* is implicitly cancelled.
* E.g. phone is locked, switch to another app or press outside the dialog.
*/
public static final int RESULT_CANCELED = 0;
/**
* The result code to propagate back to the user activity, indicates the association dialog
* is explicitly declined by the users.
*/
public static final int RESULT_USER_REJECTED = 1;
/**
* The result code to propagate back to the user activity, indicates the association
* dialog is dismissed if there's no device found after 20 seconds.
*/
public static final int RESULT_DISCOVERY_TIMEOUT = 2;
/**
* The result code to propagate back to the user activity, indicates the internal error
* in CompanionDeviceManager.
*/
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 devices 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 there's
* an internal error.
*
* @hide
*/
public static final String REASON_INTERNAL_ERROR = "internal_error";
/**
* Requesting applications will receive the String in {@link Callback#onFailure} if the
* association dialog is implicitly cancelled. E.g. phone is locked, switch to
* another app or press outside the dialog.
*
* @hide
*/
public static final String REASON_CANCELED = "canceled";
/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_CALL_METADATA,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DataSyncTypes {}
/**
* Used by {@link #enableSystemDataSyncForTypes(int, int)}}.
* Sync call metadata like muting, ending and silencing a call.
*
*/
public static final int FLAG_CALL_METADATA = 1;
/**
* A device, returned in the activity result of the {@link IntentSender} received in
* {@link Callback#onDeviceFound}
*
* Type is:
* <ul>
* <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li>
* <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li>
* <li>for WiFi - {@link android.net.wifi.ScanResult}</li>
* </ul>
*
* @deprecated use {@link AssociationInfo#getAssociatedDevice()} instead.
*/
@Deprecated
public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
/**
* Extra field name for the {@link AssociationInfo} object, included into
* {@link android.content.Intent} which application receive in
* {@link Activity#onActivityResult(int, int, Intent)} after the application's
* {@link AssociationRequest} was successfully processed and an association was created.
*/
public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION";
/**
* Test message type without a designated callback.
*
* @hide
*/
public static final int MESSAGE_REQUEST_PING = 0x63807378; // ?PIN
/**
* Test message type without a response.
*
* @hide
*/
public static final int MESSAGE_ONEWAY_PING = 0x43807378; // +PIN
/**
* Message header assigned to the remote authentication handshakes.
*
* @hide
*/
public static final int MESSAGE_REQUEST_REMOTE_AUTHENTICATION = 0x63827765; // ?RMA
/**
* Message header assigned to the telecom context sync metadata.
*
* @hide
*/
public static final int MESSAGE_REQUEST_CONTEXT_SYNC = 0x63678883; // ?CXS
/**
* Message header assigned to the permission restore request.
*
* @hide
*/
public static final int MESSAGE_REQUEST_PERMISSION_RESTORE = 0x63826983; // ?RES
/**
* Message header assigned to the one-way message sent from the wearable device.
*
* @hide
*/
public static final int MESSAGE_ONEWAY_FROM_WEARABLE = 0x43708287; // +FRW
/**
* Message header assigned to the one-way message sent to the wearable device.
*
* @hide
*/
public static final int MESSAGE_ONEWAY_TO_WEARABLE = 0x43847987; // +TOW
/**
* The length limit of Association tag.
* @hide
*/
private static final int ASSOCIATION_TAG_LENGTH_LIMIT = 1024;
/**
* Callback for applications to receive updates about and the outcome of
* {@link AssociationRequest} issued via {@code associate()} call.
*
* <p>
* 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:
* <ul>
* <li>
* choose the device to associate the application with (if multiple eligible devices are
* available)
* </li>
* <li>confirm the association</li>
* <li>
* approve the privileges the application will be granted if the association is to be created
* </li>
* </ul>
*
* If the Companion Device Manager Service needs to scan for the devices, the {@link Activity}
* will also display the status and the progress of the scan.
*
* Note that Companion Device Manager Service will only start the scanning after the
* {@link Activity} was launched and became visible.
*
* Applications are expected to launch the UI using the received {@link IntentSender} via
* {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int)}.
* </p>
*
* <p>
* 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}.
* <pre>
* <code>
* if (resultCode == Activity.RESULT_OK) {
* AssociationInfo associationInfo = data.getParcelableExtra(EXTRA_ASSOCIATION);
* }
* </code>
* </pre>
* </p>
*
* <p>
* 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}.
* </p>
*
* <p>
* 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.
* </p>
*
* @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 ArrayList<OnAssociationsChangedListenerProxy> mListeners = new ArrayList<>();
@GuardedBy("mTransportsChangedListeners")
private final ArrayList<OnTransportsChangedListenerProxy> mTransportsChangedListeners =
new ArrayList<>();
@GuardedBy("mTransports")
private final SparseArray<Transport> mTransports = new SparseArray<>();
/** @hide */
public CompanionDeviceManager(
@Nullable ICompanionDeviceManager service, @NonNull Context context) {
mService = service;
mContext = context;
}
/**
* Request to associate this app with a companion device.
*
* <p>Note that before creating establishing association the system may need to show UI to
* collect user confirmation.</p>
*
* <p>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.</p>
*
* <p>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</p>
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
**
* @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.
*
* <p>Note that before creating establishing association the system may need to show UI to
* collect user confirmation.</p>
*
* <p>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.</p>
*
* <p>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</p>
*
* <p>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.</p>
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
**
* @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.
*
* <p>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</p>
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @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();
}
}
/**
* <p>Enable system data sync (it only supports call metadata sync for now).
* By default all supported system data types are enabled.</p>
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @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();
}
}
/**
* <p>Disable system data sync (it only supports call metadata sync for now).
* By default all supported system data types are enabled.</p>
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @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();
}
}
/**
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @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 List<String> getAssociations() {
return CollectionUtils.mapNotNull(getMyAssociations(),
a -> a.isSelfManaged() ? null : a.getDeviceMacAddressAsString());
}
/**
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @return a list of associations that have been previously associated with the current app.
*/
@UserHandleAware
@NonNull
public List<AssociationInfo> getMyAssociations() {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return Collections.emptyList();
}
try {
return mService.getAssociations(mContext.getOpPackageName(), mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Remove the association between this app and the device with the given mac address.
*
* <p>Any privileges provided via being associated with a given device will be revoked</p>
*
* <p>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</p>
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @param deviceMacAddress the MAC address of device to disassociate from this app. Device
* address is case-sensitive in API level &lt; 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.
*
* <p>Any privileges provided via being associated with a given device will be revoked</p>
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @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.
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*/
@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
*
* <p>Calling this API requires a uses-feature
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
*
* @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.
*
* <p>This is a system API protected by the
* {@link android.Manifest.permission#MANAGE_COMPANION_DEVICES} permission, thats 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.</p>
*
* <p>Note if the caller has the
* {@link android.Manifest.permission#COMPANION_APPROVE_WIFI_CONNECTIONS} permission, this
* method will return true by default.</p>
*
* @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 List<AssociationInfo> getAllAssociations() {
return getAllAssociations(mContext.getUserId());
}
/**
* Per-user version of {@link #getAllAssociations()}.
*
* @hide
*/
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
@NonNull
public List<AssociationInfo> getAllAssociations(@UserIdInt int userId) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return Collections.emptyList();
}
try {
return mService.getAllAssociationsForUser(userId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Listener for any changes to {@link AssociationInfo}.
*
* @hide
*/
@SystemApi
public interface OnAssociationsChangedListener {
/**
* Invoked when a change occurs to any of the associations for the user (including adding
* new associations and removing existing associations).
*
* @param associations all existing associations for the user (after the change).
*/
void onAssociationsChanged(@NonNull List<AssociationInfo> associations);
}
/**
* Register listener for any changes to {@link AssociationInfo}.
*
* @see #getAllAssociations()
* @hide
*/
@SystemApi
@UserHandleAware
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
public void addOnAssociationsChangedListener(
@NonNull Executor executor, @NonNull OnAssociationsChangedListener listener) {
addOnAssociationsChangedListener(executor, listener, mContext.getUserId());
}
/**
* Per-user version of
* {@link #addOnAssociationsChangedListener(Executor, OnAssociationsChangedListener)}.
*
* @hide
*/
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
public void addOnAssociationsChangedListener(
@NonNull Executor executor, @NonNull OnAssociationsChangedListener listener,
@UserIdInt int userId) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
synchronized (mListeners) {
final OnAssociationsChangedListenerProxy proxy = new OnAssociationsChangedListenerProxy(
executor, listener);
try {
mService.addOnAssociationsChangedListener(proxy, userId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
mListeners.add(proxy);
}
}
/**
* Unregister listener for any changes to {@link AssociationInfo}.
*
* @see #getAllAssociations()
* @hide
*/
@SystemApi
@UserHandleAware
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
public void removeOnAssociationsChangedListener(
@NonNull OnAssociationsChangedListener listener) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
synchronized (mListeners) {
final Iterator<OnAssociationsChangedListenerProxy> iterator = mListeners.iterator();
while (iterator.hasNext()) {
final OnAssociationsChangedListenerProxy proxy = iterator.next();
if (proxy.mListener == listener) {
try {
mService.removeOnAssociationsChangedListener(proxy, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
iterator.remove();
}
}
}
}
/**
* Adds a listener for any changes to the list of attached transports.
* Registered listener will be triggered with a list of existing transports when a transport
* is detached or a new transport is attached.
*
* @param executor The executor which will be used to invoke the listener.
* @param listener Called when a transport is attached or detached. Contains the updated list of
* associations which have connected transports.
* @see com.android.server.companion.transport.Transport
* @hide
*/
@RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
public void addOnTransportsChangedListener(
@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<List<AssociationInfo>> listener) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
synchronized (mTransportsChangedListeners) {
final OnTransportsChangedListenerProxy proxy = new OnTransportsChangedListenerProxy(
executor, listener);
try {
mService.addOnTransportsChangedListener(proxy);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
mTransportsChangedListeners.add(proxy);
}
}
/**
* Removes the registered listener for any changes to the list of attached transports.
*
* @see com.android.server.companion.transport.Transport
*
* @hide
*/
@RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
public void removeOnTransportsChangedListener(
@NonNull Consumer<List<AssociationInfo>> listener) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
synchronized (mTransportsChangedListeners) {
final Iterator<OnTransportsChangedListenerProxy> iterator =
mTransportsChangedListeners.iterator();
while (iterator.hasNext()) {
final OnTransportsChangedListenerProxy proxy = iterator.next();
if (proxy.mListener == listener) {
try {
mService.removeOnTransportsChangedListener(proxy);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
iterator.remove();
}
}
}
}
/**
* Sends a message to associated remote devices. The target associations must already have a
* connected transport.
*
* @see #attachSystemDataTransport(int, InputStream, OutputStream)
*
* @hide
*/
@RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
public void sendMessage(int messageType, @NonNull byte[] data, @NonNull int[] associationIds) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
try {
mService.sendMessage(messageType, data, associationIds);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Adds a listener that triggers when messages of given type are received.
*
* @param executor The executor which will be used to invoke the listener.
* @param messageType Message type to be subscribed to.
* @param listener Called when a message is received. Contains the association ID of the message
* sender and the message payload as a byte array.
* @hide
*/
@RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
public void addOnMessageReceivedListener(
@NonNull @CallbackExecutor Executor executor, int messageType,
@NonNull BiConsumer<Integer, byte[]> listener) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy(
executor, listener);
try {
mService.addOnMessageReceivedListener(messageType, proxy);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Removes the registered listener for received messages of given type.
*
* @hide
*/
@RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
public void removeOnMessageReceivedListener(int messageType,
@NonNull BiConsumer<Integer, byte[]> listener) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy(
null, listener);
try {
mService.removeOnMessageReceivedListener(messageType, proxy);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Checks whether the bluetooth device represented by the mac address was recently associated
* with the companion app. This allows these devices to skip the Bluetooth pairing dialog if
* their pairing variant is {@link BluetoothDevice#PAIRING_VARIANT_CONSENT}.
*
* @param packageName the package name of the calling app
* @param deviceMacAddress the bluetooth device's mac address
* @param user the user handle that currently hosts the package being queried for a companion
* device association
* @return true if it was recently associated and we can bypass the dialog, false otherwise
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
public boolean canPairWithoutPrompt(@NonNull String packageName,
@NonNull String deviceMacAddress, @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(deviceMacAddress, "device mac address cannot be null");
Objects.requireNonNull(user, "user handle cannot be null");
try {
return mService.canPairWithoutPrompt(packageName, deviceMacAddress,
user.getIdentifier());
} 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.
*
* <p>The provided device must be {@link #associate associated} with the calling app before
* calling this method.</p>
*
* <p>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.</p>
*
* <p>Calling app must declare uses-permission
* {@link android.Manifest.permission#REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE}.</p>
*
* <p>Calling app must check for feature presence of
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.</p>
*
* <p>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.</p>
*
* <p>For Bluetooth classic devices this is triggered when the device connects/disconnects.
* WiFi devices are not supported.</p>
*
* <p>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.</p>
*
* @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.
*
* <p>The app doesn't need to remain running in order to receive its callbacks.</p>
*
* <p>Calling app must check for feature presence of
* {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.</p>
*
* <p>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.</p>
*
* <p>For Bluetooth classic devices this is triggered when the device connects/disconnects.</p>
*
* <p>WiFi devices are not supported.</p>
*
* <p>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.</p>
*
* @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[])}
*
* <p>Calling app must declare uses-permission
* {@link android.Manifest.permission#DELIVER_COMPANION_MESSAGES}</p>
*
* @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
*
* <p>This API is only available for the companion apps that manage the connectivity by
* themselves.</p>
*
* @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.
*
* <p>This API is only available for the companion apps that manage the connectivity by
* themselves.</p>
*
* @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.
*
* <p>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)}.</p>
*
* <p>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}.</p>
*
* @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.
*
* <p>
* 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.
* </p>
*
* @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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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 OutcomeReceiver<Void, CompanionException> result)
throws DeviceNotAssociatedException {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
try {
mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(),
associationId, new SystemDataTransferCallbackProxy(executor, result));
} catch (RemoteException e) {
ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class);
throw e.rethrowFromSystemServer();
}
}
/**
* Checks whether the calling companion application is currently bound.
*
* @return true if application is bound, false otherwise
* @hide
*/
@UserHandleAware
public boolean isCompanionApplicationBound() {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return false;
}
try {
return mService.isCompanionApplicationBound(
mContext.getOpPackageName(), mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Enables or disables secure transport for testing. Defaults to being enabled.
* Should not be used outside of testing.
*
* @param enabled true to enable. false to disable.
* @hide
*/
@TestApi
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
public void enableSecureTransport(boolean enabled) {
if (mService == null) {
Log.w(TAG, "CompanionDeviceManager service is not available.");
return;
}
try {
mService.enableSecureTransport(enabled);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Sets the {@link AssociationInfo#getTag() tag} for this association.
*
* <p>The length of the tag must be at most 1024 characters to save disk space.
*
* <p>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.
*
* <p>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 <T> void execute(Consumer<T> callback, T arg) {
if (mExecutor != null) {
mExecutor.execute(() -> callback.accept(arg));
} else if (mHandler != null) {
mHandler.post(() -> callback.accept(arg));
}
}
}
private static class OnAssociationsChangedListenerProxy
extends IOnAssociationsChangedListener.Stub {
private final Executor mExecutor;
private final OnAssociationsChangedListener mListener;
private OnAssociationsChangedListenerProxy(Executor executor,
OnAssociationsChangedListener listener) {
mExecutor = executor;
mListener = listener;
}
@Override
public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) {
mExecutor.execute(() -> mListener.onAssociationsChanged(associations));
}
}
private static class OnTransportsChangedListenerProxy
extends IOnTransportsChangedListener.Stub {
private final Executor mExecutor;
private final Consumer<List<AssociationInfo>> mListener;
private OnTransportsChangedListenerProxy(Executor executor,
Consumer<List<AssociationInfo>> listener) {
mExecutor = executor;
mListener = listener;
}
@Override
public void onTransportsChanged(@NonNull List<AssociationInfo> associations) {
mExecutor.execute(() -> mListener.accept(associations));
}
}
private static class OnMessageReceivedListenerProxy
extends IOnMessageReceivedListener.Stub {
private final Executor mExecutor;
private final BiConsumer<Integer, byte[]> mListener;
private OnMessageReceivedListenerProxy(Executor executor,
BiConsumer<Integer, byte[]> listener) {
mExecutor = executor;
mListener = listener;
}
@Override
public void onMessageReceived(int associationId, byte[] data) {
mExecutor.execute(() -> mListener.accept(associationId, data));
}
}
private static class SystemDataTransferCallbackProxy extends ISystemDataTransferCallback.Stub {
private final Executor mExecutor;
private final OutcomeReceiver<Void, CompanionException> mCallback;
private SystemDataTransferCallbackProxy(Executor executor,
OutcomeReceiver<Void, CompanionException> callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void onResult() {
mExecutor.execute(() -> mCallback.onResult(null));
}
@Override
public void onError(String error) {
mExecutor.execute(() -> mCallback.onError(new CompanionException(error)));
}
}
/**
* Representation of an active system data transport.
* <p>
* 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();
}
}
}
}