/* * 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 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.os.Binder; 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. *
* 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}. *
* Communication between apps with different package names is not supported. *
* A common use case of this API is like: *
* ========================================== ========================================= * = client1 (occupantZone1) = = client2 (occupantZone2) = * = = = = * = ************ ************ = = ************ ************ = * = * sender1A * * sender1B * = = * sender2A * * sender2B * = * = ************ ************ = = ************ ************ = * = = = = * = **************************** = = **************************** = * = * ReceiverService1 * = = * ReceiverService2 * = * = **************************** = = **************************** = * = = = = * = ************** ************** = = ************** ************** = * = * receiver1A * * receiver1B * = = * receiver2A * * receiver2B * = * = ************** ************** = = ************** ************** = * ========================================== ========================================= * * ****** Payload ***** * * ID: "receiver2A" * * * value: "123" * * ******************** Payload |---> receiver2A * sender1A -------------------------->ReceiverService2--------------->| * |.... receiver2B **
* 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. *
* 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: *
* 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. */ 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 */ 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. *
* Once disconnected, the sender can no longer send {@link Payload} to the receiver
* client.
*/
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}.
*/
void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload);
}
/** An exception to indicate that it failed to send the {@link Payload}. */
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
* 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
*/
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void registerReceiver(@NonNull String receiverEndpointId,
@NonNull @CallbackExecutor Executor executor,
@NonNull PayloadCallback callback) {
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}.
*
* 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
*/
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void unregisterReceiver(@NonNull String receiverEndpointId) {
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.
*
* 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.
*
* 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.
*
* The caller may call {@link #cancelConnection} to cancel the request.
*
* 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.
*
* 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}
*/
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void requestConnection(@NonNull OccupantZoneInfo receiverZone,
@NonNull @CallbackExecutor Executor executor,
@NonNull ConnectionRequestCallback callback) {
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}.
*
* 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}
*/
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void cancelConnection(@NonNull OccupantZoneInfo receiverZone) {
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}.
*
* 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.
*/
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void sendPayload(@NonNull OccupantZoneInfo receiverZone, @NonNull Payload payload)
throws PayloadTransferException {
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}.
*
* 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}.
*
* 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]")
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public void disconnect(@NonNull OccupantZoneInfo receiverZone) {
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.
*
* 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]")
@RequiresPermission(Car.PERMISSION_MANAGE_OCCUPANT_CONNECTION)
public boolean isConnected(@NonNull OccupantZoneInfo receiverZone) {
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);
}
}
}