blob: 7285e68db0dce2e5785ec600ce7030b734174bcb [file] [log] [blame]
/*
* Copyright (C) 2022 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.car.occupantconnection;
import static com.android.car.internal.util.VersionUtils.assertPlatformVersionAtLeastU;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.car.Car;
import android.car.CarManagerBase;
import android.car.CarOccupantZoneManager.OccupantZoneInfo;
import android.car.CarRemoteDeviceManager.AppState;
import android.car.CarRemoteDeviceManager.OccupantZoneState;
import android.car.annotation.ApiRequirements;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* API for communication between different endpoints in the occupant zones in the car.
* <p>
* Unless specified explicitly, a client means an app that uses this API and runs as a
* foreground user in an occupant zone, while a peer client means an app that has the same package
* name as the caller app and runs as another foreground user (in another occupant zone or even
* another Android system).
* An endpoint means a component (such as a Fragment or an Activity) that has an instance of
* {@link CarOccupantConnectionManager}.
* <p>
* Communication between apps with different package names is not supported.
* <p>
* A common use case of this API is like:
* <pre>
* ========================================== =========================================
* = client1 (occupantZone1) = = client2 (occupantZone2) =
* = = = =
* = ************ ************ = = ************ ************ =
* = * sender1A * * sender1B * = = * sender2A * * sender2B * =
* = ************ ************ = = ************ ************ =
* = = = =
* = **************************** = = **************************** =
* = * ReceiverService1 * = = * ReceiverService2 * =
* = **************************** = = **************************** =
* = = = =
* = ************** ************** = = ************** ************** =
* = * receiver1A * * receiver1B * = = * receiver2A * * receiver2B * =
* = ************** ************** = = ************** ************** =
* ========================================== =========================================
*
* ****** Payload *****
* * ID: "receiver2A" *
* * value: "123" *
* ******************** Payload |---> receiver2A
* sender1A -------------------------->ReceiverService2--------------->|
* |.... receiver2B
* </pre>
* <ul>
* <li> Client1 and client2 must have the same package name. Client1 runs in occupantZone1
* while client2 runs in occupantZone2. Sender1A (an endpoint in client1) wants to
* send a {@link Payload} to receiver2A (an endpoint in client2).
* <li> Pre-connection:
* <ul>
* <li> The client app inherits {@link AbstractReceiverService} and declares the service in
* its manifest file.
* </ul>
* <li> Establish connection:
* <ul>
* <li> Sender1A monitors occupantZone2 by calling {@link
* android.car.CarRemoteDeviceManager#registerStateCallback}.
* <li> Sender1A waits until the {@link OccupantZoneState} of occupantZone2 becomes
* {@link android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_CONNECTION_READY} and
* the {@link AppState} of client2 becomes {@link
* android.car.CarRemoteDeviceManager#FLAG_CLIENT_INSTALLED}, then requests a connection
* to occupantZone2 by calling {@link #requestConnection}. If UI is needed to establish
* the connection, sender1A must wait until {@link
* android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED} and {@link
* android.car.CarRemoteDeviceManager#FLAG_CLIENT_IN_FOREGROUND}).
* <li> ReceiverService2 is started and bound by car service ({@link
* com.android.car.occupantconnection.CarOccupantConnectionService} automatically.
* ReceiverService2 is notified via {@link
* AbstractReceiverService#onConnectionInitiated}.
* <li> ReceiverService2 accepts the connection by calling {@link
* AbstractReceiverService#acceptConnection}.
* <li> Then the one-way connection is established. Sender1A is notified via {@link
* ConnectionRequestCallback#onConnected}, and ReceiverService2 is notified via
* {@link AbstractReceiverService#onConnected}.
* </ul>
* <li> Send Payload:
* <ul>
* <li> Sender1A sends a Payload to occupantZone2 by calling {@link #sendPayload}. To indicate
* that the Payload is sent to receiver2A, Sender1A puts receiver2A's ID ("receiver2A")
* into the Payload.
* <li> ReceiverService2 is notified for the Payload via {@link
* AbstractReceiverService#onPayloadReceived}.
* In this method, ReceiverService2 can forward the Payload to client2's receiver
* endpoints (if any), or cache the Payload and forward it later once a new receiver
* endpoint is registered.
* </ul>
* <li> Register receiver:
* <ul>
* <li> Receiver2A calls {@link #registerReceiver} with ID "receiver2A". Then
* ReceiverService2 is notified via {@link AbstractReceiverService#onReceiverRegistered}.
* In that method, ReceiverService2 parses the Payload and finds that the Payload should
* be sent to the endpoint with ID "receiver2A", then invokes {@link
* AbstractReceiverService#forwardPayload} to forward the cached Payload to receiver2A.
* <p>
* Note: this step can be done before "Establish connection". In this case,
* ReceiverService2 will be started and bound by car service early.
* Once sender1A sends a Payload to occupantZone2, ReceiverService2 will be notified
* via {@link AbstractReceiverService#onReceiverRegistered}. In that method,
* ReceiverService2 can forward the Payload to Receiver2A without caching.
* <li> Receiver2A is notified for the Payload via {@link PayloadCallback#onPayloadReceived}.
* </ul>
* <li> Terminate the connection:
* <ul>
* <li> Sender1A terminates the connection to occupantZone2:
* Once sender1A no longer needs to send Payload to occupantZone2, it terminates the
* connection by calling {@link #disconnect}. Then sender1A is notified via
* {@link ConnectionRequestCallback#onDisconnected}, and ReceiverService2 is notified via
* {@link AbstractReceiverService#onDisconnected}.
* <li> Unregister receiver2A:
* Once receiver2A no longer needs to receive Payload from any other occupant zones,
* it calls {@link #unregisterReceiver}.
* <li> Unbound and destroy ReceiverService2:
* Since all the senders have disconnected from occupantZone2 and there is no receiver
* registered in occupantZone2, ReceiverService2 will be unbound and destroyed
* automatically.
* </ul>
* <li> Sender1A stops monitoring other occupant zones by calling {@link
* android.car.CarRemoteDeviceManager#unregisterStateCallback}. This step can
* be done before or after "Terminate the connection".
* </ul>
* <p>
* For a given {@link android.car.Car} instance, the CarOccupantConnectionManager is a singleton.
* However, the client app may create multiple {@link android.car.Car} instances thus create
* multiple CarOccupantConnectionManager instances. These CarOccupantConnectionManager instances
* are treated as the same instance for the client app. For example:
* <ul>
* <li> Sender1A creates a CarOccupantConnectionManager instance (managerA), while sender1B
* creates a different CarOccupantConnectionManager instance (managerB). Then sender1A uses
* managerA to request a connection to occupantZone2. Once connected, sender1B can use
* managerB to send Payload to occupantZone2 without requesting a new connection.
* To know whether it is connected to occupantZone2, sender1B can call {@link #isConnected}.
* <li> Besides, sender1B can terminate the connection by calling managerB#disconnect(), despite
* that the connection was requested by sender1A. Once the connection is terminated, sender1A
* will be notified via {@link ConnectionRequestCallback#onDisconnected}, and sender1B will
* not be notified since it didn't register register the {@link ConnectionRequestCallback}.
* </ul>
*
* @hide
*/
@SystemApi
public final class CarOccupantConnectionManager extends CarManagerBase {
private static final String TAG = CarOccupantConnectionManager.class.getSimpleName();
/** The connection request has no error. */
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_NONE = 0;
/** The connection request failed because of an error of unidentified cause. */
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_UNKNOWN = 1;
/**
* The connection request failed because the peer occupant zone was not ready for connection.
* To avoid this error, the caller endpoint should ensure that the state of the peer occupant
* zone is {@link android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_CONNECTION_READY} before
* requesting a connection to it.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_NOT_READY = 2;
/**
* The connection request failed because the peer app was not installed. To avoid this error,
* the caller endpoint should ensure that the state of the peer app is {@link
* android.car.CarRemoteDeviceManager#FLAG_CLIENT_INSTALLED} before requesting a connection to
* it.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_PEER_APP_NOT_INSTALLED = 3;
/**
* The connection request failed because its long version code ({@link
* PackageInfo#getLongVersionCode}) didn't match the peer app's long version code.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_LONG_VERSION_NOT_MATCH = 4;
/**
* The connection request failed because its signing info ({@link PackageInfo#signingInfo}
* didn't match the peer app's signing info.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_SIGNATURE_NOT_MATCH = 5;
/** The connection request failed because the user rejected it. */
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_USER_REJECTED = 6;
/**
* The maximum value of predefined connection error code. If the client app wants to pass a
* custom value in {@link AbstractReceiverService#rejectConnection}, the custom value must be
* larger than this value, otherwise the sender client might get the wrong connection error code
* when its connection request fails.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final int CONNECTION_ERROR_PREDEFINED_MAXIMUM_VALUE = 10000;
/**
* Flags for the error type of connection request.
*
* @hide
*/
@IntDef(flag = false, prefix = {"CONNECTION_ERROR_"}, value = {
CONNECTION_ERROR_NONE,
CONNECTION_ERROR_UNKNOWN,
CONNECTION_ERROR_NOT_READY,
CONNECTION_ERROR_PEER_APP_NOT_INSTALLED,
CONNECTION_ERROR_LONG_VERSION_NOT_MATCH,
CONNECTION_ERROR_SIGNATURE_NOT_MATCH,
CONNECTION_ERROR_USER_REJECTED,
CONNECTION_ERROR_PREDEFINED_MAXIMUM_VALUE
})
@Retention(RetentionPolicy.SOURCE)
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public @interface ConnectionError {
}
/**
* A callback for lifecycle events of a connection request. When the endpoint (sender) calls
* {@link #requestConnection} to connect to its peer client, it will be notified for the events.
* The sender may call {@link #cancelConnection} if none of the events are triggered for a
* long time.
*/
public interface ConnectionRequestCallback {
/**
* Invoked when the one-way connection has been established.
* <p>
* In order to establish the connection, the receiver {@link AbstractReceiverService}
* must accept the connection, and the sender must not cancel the request before the
* connection is established.
* Once the connection is established, the sender can send {@link Payload} to the
* receiver client.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
void onConnected(@NonNull OccupantZoneInfo receiverZone);
/**
* Invoked when there was an error when establishing the connection. For example, the
* receiver client is not ready for connection, or the receiver client rejected the
* connection request.
*
* @param connectionError could be any value of {@link ConnectionError}, or an app-defined
* value
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
void onFailed(@NonNull OccupantZoneInfo receiverZone, int connectionError);
/**
* Invoked when the connection is terminated. For example, the receiver {@link
* AbstractReceiverService} is unbound and destroyed, is crashed, or the receiver client
* has become unreachable.
* <p>
* Once disconnected, the sender can no longer send {@link Payload} to the receiver
* client.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
void onDisconnected(@NonNull OccupantZoneInfo receiverZone);
}
/** A callback to receive a {@link Payload}. */
public interface PayloadCallback {
/**
* Invoked when the receiver endpoint has received a {@link Payload} from {@code
* senderZone}.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload);
}
/** An exception to indicate that it failed to send the {@link Payload}. */
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public static final class PayloadTransferException extends Exception {
}
private final ICarOccupantConnection mService;
private final Object mLock = new Object();
private final String mPackageName;
/**
* A map of connection requests. The key is the zone ID of the receiver occupant zone, and
* the value is the callback and associated executor.
*/
@GuardedBy("mLock")
private final SparseArray<Pair<ConnectionRequestCallback, Executor>>
mConnectionRequestMap = new SparseArray<>();
private final IConnectionRequestCallback mBinderConnectionRequestCallback =
new IConnectionRequestCallback.Stub() {
@Override
public void onConnected(OccupantZoneInfo receiverZone) {
synchronized (mLock) {
Pair<ConnectionRequestCallback, Executor> pair =
mConnectionRequestMap.get(receiverZone.zoneId);
if (pair == null) {
Slog.e(TAG, "onConnected: no pending connection request");
return;
}
// Notify the sender of success.
ConnectionRequestCallback callback = pair.first;
Executor executor = pair.second;
executor.execute(() -> callback.onConnected(receiverZone));
// Unlike other onFoo() methods, we shouldn't remove the callback here
// because we need to invoke it once it is disconnected.
}
}
@Override
public void onFailed(OccupantZoneInfo receiverZone, int connectionError) {
synchronized (mLock) {
Pair<ConnectionRequestCallback, Executor> pair =
mConnectionRequestMap.get(receiverZone.zoneId);
if (pair == null) {
Slog.e(TAG, "onFailed: no pending connection request");
return;
}
// Notify the sender of failure.
ConnectionRequestCallback callback = pair.first;
Executor executor = pair.second;
executor.execute(() -> callback.onFailed(receiverZone, connectionError));
mConnectionRequestMap.remove(receiverZone.zoneId);
}
}
@Override
public void onDisconnected(OccupantZoneInfo receiverZone) {
synchronized (mLock) {
Pair<ConnectionRequestCallback, Executor> pair =
mConnectionRequestMap.get(receiverZone.zoneId);
if (pair == null) {
Slog.e(TAG, "onDisconnected: no pending connection request");
return;
}
// Notify the sender of disconnection.
ConnectionRequestCallback callback = pair.first;
Executor executor = pair.second;
executor.execute(() -> callback.onDisconnected(receiverZone));
mConnectionRequestMap.remove(receiverZone.zoneId);
}
}
};
/**
* A map of registered receivers. The key is the endpointId of the receiver, the value is
* the associated callback and the Executor of the callback.
*/
@GuardedBy("mLock")
private final ArrayMap<String, Pair<PayloadCallback, Executor>> mReceiverPayloadCallbackMap =
new ArrayMap<>();
private final IPayloadCallback mBinderPayloadCallback = new IPayloadCallback.Stub() {
@Override
public void onPayloadReceived(OccupantZoneInfo senderZone, String receiverEndpointId,
Payload payload) {
Pair<PayloadCallback, Executor> pair;
synchronized (mLock) {
pair = mReceiverPayloadCallbackMap.get(receiverEndpointId);
if (pair == null) {
// This should never happen, but let's be cautious.
Slog.e(TAG, "Couldn't find receiver " + receiverEndpointId);
return;
}
}
PayloadCallback callback = pair.first;
Executor executor = pair.second;
executor.execute(() -> callback.onPayloadReceived(senderZone, payload));
}
};
/** @hide */
public CarOccupantConnectionManager(Car car, IBinder service) {
super(car);
mService = ICarOccupantConnection.Stub.asInterface(service);
mPackageName = mCar.getContext().getPackageName();
}
/** @hide */
@Override
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
public void onCarDisconnected() {
assertPlatformVersionAtLeastU();
synchronized (mLock) {
mConnectionRequestMap.clear();
mReceiverPayloadCallbackMap.clear();
}
}
/**
* Registers a {@link PayloadCallback} to receive {@link Payload}. If the {@link
* AbstractReceiverService} in the caller app was not started yet, it will be started and
* bound by car service automatically.
* <p>
* The caller endpoint must call {@link #unregisterReceiver} before it is destroyed.
*
* @param receiverEndpointId the ID of this receiver endpoint. Since there might be multiple
* receiver endpoints in the client app, the ID can be used by the
* client app ({@link AbstractReceiverService}) to decide which
* endpoint(s) to dispatch the Payload to. The client app can use any
* String as the ID, as long as it is unique among the client app.
* @param executor the Executor to run the callback
* @param callback the callback notified when this endpoint receives a Payload
* @throws IllegalStateException if the {@code receiverEndpointId} had a {@link PayloadCallback}
* registered
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void registerReceiver(@NonNull String receiverEndpointId,
@NonNull @CallbackExecutor Executor executor,
@NonNull PayloadCallback callback) {
assertPlatformVersionAtLeastU();
Objects.requireNonNull(receiverEndpointId, "receiverEndpointId cannot be null");
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
synchronized (mLock) {
try {
mService.registerReceiver(mPackageName, receiverEndpointId, mBinderPayloadCallback);
// Save the callback only after the remote call succeeded.
mReceiverPayloadCallbackMap.put(receiverEndpointId, new Pair<>(callback, executor));
} catch (RemoteException e) {
Slog.e(TAG, "Failed to register receiver: " + receiverEndpointId);
handleRemoteExceptionFromCarService(e);
}
}
}
/**
* Unregisters the existing {@link PayloadCallback} for {@code receiverEndpointId}.
* <p>
* This method can be called after calling {@link #registerReceiver} once the receiver
* endpoint no longer needs to receive Payload, or becomes inactive.
* This method must be called before the receiver endpoint is destroyed. Failing to call this
* method might cause the AbstractReceiverService to persist.
*
* @throws IllegalStateException if the {@code receiverEndpointId} had no {@link
* PayloadCallback} registered
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void unregisterReceiver(@NonNull String receiverEndpointId) {
assertPlatformVersionAtLeastU();
Objects.requireNonNull(receiverEndpointId, "receiverEndpointId cannot be null");
synchronized (mLock) {
try {
mService.unregisterReceiver(mPackageName, receiverEndpointId);
// Remove the callback after the remote call succeeded.
mReceiverPayloadCallbackMap.remove(receiverEndpointId);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to unregister receiver: " + receiverEndpointId);
handleRemoteExceptionFromCarService(e);
}
}
}
/**
* Sends a request to connect to the receiver client in {@code receiverZone}. The {@link
* AbstractReceiverService} in the receiver client will be started and bound automatically if it
* was not started yet.
* <p>
* This method should only be called when the state of the {@code receiverZone} contains
* {@link android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_CONNECTION_READY} (and
* {@link android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED} and {@link
* android.car.CarRemoteDeviceManager#FLAG_CLIENT_IN_FOREGROUND} if UI is needed to
* establish the connection). Otherwise, errors may occur.
* <p>
* For security, it is highly recommended that the sender not request a connection to the
* receiver client if the state of the receiver client doesn't contain
* {@link android.car.CarRemoteDeviceManager#FLAG_CLIENT_SAME_LONG_VERSION} or
* {@link android.car.CarRemoteDeviceManager#FLAG_CLIENT_SAME_SIGNATURE}. If the sender still
* wants to request the connection in the case above, it should call
* {@link android.car.CarRemoteDeviceManager#getEndpointPackageInfo} to get the receiver's
* {@link android.content.pm.PackageInfo} and check if it's valid before requesting the
* connection.
* <p>
* The caller may call {@link #cancelConnection} to cancel the request.
* <p>
* The connection is one-way. In other words, the receiver can't send {@link Payload} to the
* sender. If the receiver wants to send {@link Payload}, it must call this method to become
* a sender.
* <p>
* The caller must not request another connection to the same {@code receiverZone} if there
* is an established connection or pending connection (a connection request that has not been
* responded yet) to {@code receiverZone}.
* The caller must call {@link #disconnect} before it is destroyed.
*
* @param receiverZone the occupant zone to connect to
* @param executor the Executor to run the callback
* @param callback the callback notified for the request result
* @throws IllegalStateException if there is an established connection or pending connection to
* {@code receiverZone}
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void requestConnection(@NonNull OccupantZoneInfo receiverZone,
@NonNull @CallbackExecutor Executor executor,
@NonNull ConnectionRequestCallback callback) {
assertPlatformVersionAtLeastU();
Objects.requireNonNull(receiverZone, "receiverZone cannot be null");
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
synchronized (mLock) {
Preconditions.checkState(!mConnectionRequestMap.contains(receiverZone.zoneId),
"Already requested a connection to " + receiverZone);
try {
mService.requestConnection(mPackageName, receiverZone,
mBinderConnectionRequestCallback);
mConnectionRequestMap.put(receiverZone.zoneId, new Pair<>(callback, executor));
} catch (RemoteException e) {
Slog.e(TAG, "Failed to request connection");
handleRemoteExceptionFromCarService(e);
}
}
}
/**
* Cancels the pending connection request to the peer client in {@code receiverZone}.
* <p>
* The caller endpoint may call this method when it has requested a connection, but hasn't
* received any response for a long time, or the user wants to cancel the request explicitly.
* In other words, this method should be called after {@link #requestConnection}, and before
* any events in the {@link ConnectionRequestCallback} is triggered.
*
* @throws IllegalStateException if this {@link CarOccupantConnectionManager} has no pending
* connection request to {@code receiverZone}
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void cancelConnection(@NonNull OccupantZoneInfo receiverZone) {
assertPlatformVersionAtLeastU();
Objects.requireNonNull(receiverZone, "receiverZone cannot be null");
synchronized (mLock) {
Preconditions.checkState(mConnectionRequestMap.contains(receiverZone.zoneId),
"This manager instance has no connection request to " + receiverZone);
try {
mService.cancelConnection(mPackageName, receiverZone);
mConnectionRequestMap.remove(receiverZone.zoneId);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to cancel connection");
handleRemoteExceptionFromCarService(e);
}
}
}
/**
* Sends the {@code payload} to the peer client in {@code receiverZone}.
* <p>
* Different sender endpoints in the same client app are treated as the same sender. If the
* sender endpoints need to differentiate themselves, they can put the identity info into the
* payload.
*
* @throws IllegalStateException if it was not connected to the peer client in
* {@code receiverZone}
* @throws PayloadTransferException if the payload was not sent. For example, this method is
* called when the connection is not established or has been
* terminated, or an internal error occurred.
*/
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void sendPayload(@NonNull OccupantZoneInfo receiverZone, @NonNull Payload payload)
throws PayloadTransferException {
assertPlatformVersionAtLeastU();
Objects.requireNonNull(receiverZone, "receiverZone cannot be null");
Objects.requireNonNull(payload, "payload cannot be null");
try {
mService.sendPayload(mPackageName, receiverZone, payload);
} catch (IllegalStateException e) {
Slog.e(TAG, "Failed to send Payload to " + receiverZone);
throw new PayloadTransferException();
} catch (RemoteException e) {
Slog.e(TAG, "Failed to send Payload to " + receiverZone);
handleRemoteExceptionFromCarService(e);
}
}
/**
* Disconnects from the peer client in {@code receiverZone}.
* <p>
* This method can be called as soon as the caller app no longer needs to send {@link Payload}
* to {@code receiverZone}. If there are multiple sender endpoints in the client app reuse the
* same connection, this method should be called when all sender endpoints no longer need to
* send Payload to {@code receiverZone}.
* <p>
* This method must be called before the caller is destroyed. Failing to call this method might
* cause the {@link AbstractReceiverService} in the peer client to persist.
*
* @throws IllegalStateException if it was not connected to the peer client in
* {@code receiverZone}
*/
@SuppressWarnings("[NotCloseable]")
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void disconnect(@NonNull OccupantZoneInfo receiverZone) {
assertPlatformVersionAtLeastU();
Objects.requireNonNull(receiverZone, "receiverZone cannot be null");
try {
mService.disconnect(mPackageName, receiverZone);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to disconnect");
handleRemoteExceptionFromCarService(e);
}
}
/**
* Returns whether it is connected to its peer client in {@code receiverZone}. When it is
* connected, it can send {@link Payload} to the peer client.
* <p>
* Note: the connection is one-way. The peer client can not send {@link Payload} to this client
* unless the peer client is also connected to this client.
*/
@SuppressWarnings("[NotCloseable]")
@ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0,
minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0)
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public boolean isConnected(@NonNull OccupantZoneInfo receiverZone) {
assertPlatformVersionAtLeastU();
Objects.requireNonNull(receiverZone, "receiverZone cannot be null");
try {
return mService.isConnected(mPackageName, receiverZone);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to get connection state");
return handleRemoteExceptionFromCarService(e, false);
}
}
}