1966 lines
76 KiB
Java
1966 lines
76 KiB
Java
/*
|
||
* 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 < 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, 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.</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();
|
||
}
|
||
}
|
||
}
|
||
}
|