blob: 35b4f13080e8f0062050fdec6b3f74a6877b34a3 [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;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.car.CarOccupantZoneManager.OccupantZoneInfo;
import android.car.builtin.util.Slogf;
import android.car.occupantconnection.ICarRemoteDevice;
import android.car.occupantconnection.IStateCallback;
import android.content.pm.PackageInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
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 monitoring the states of occupant zones in the car, managing their power, and monitoring
* peer clients in those occupant zones.
* <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).
*
* @hide
*/
@SystemApi
public final class CarRemoteDeviceManager extends CarManagerBase {
private static final String TAG = CarRemoteDeviceManager.class.getSimpleName();
/**
* Flag to indicate whether all the displays of the occupant zone are powered on. If any of
* them is not powered on, the caller can power them on by {@link #setOccupantZonePower}.
*/
public static final int FLAG_OCCUPANT_ZONE_POWER_ON = 1 << 0;
/**
* Flag to indicate whether the main display of the occupant zone is unlocked. When it is
* locked, it can't display UI. If UI is needed to establish the connection (for example, it
* needs to show a dialog to get user approval), the caller shouldn't request a connection to
* the occupant zone when it's locked.
*/
public static final int FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED = 1 << 1;
/**
* Flag to indicate whether the occupant zone is ready for connection.
* If it is ready and the peer app is installed in it, the caller can call {@link
* android.car.occupantconnection.CarOccupantConnectionManager#requestConnection} to connect to
* the client app in the occupant zone. Note: if UI is needed, the caller should make sure the
* main display of the occupant zone is unlocked and the client app is running in the foreground
* before requesting a connection to it.
*/
public static final int FLAG_OCCUPANT_ZONE_CONNECTION_READY = 1 << 2;
/**
* Flag to indicate whether the client app is installed in the occupant zone. If it's not
* installed, the caller may show a Dialog to promote the user to install the app.
*/
public static final int FLAG_CLIENT_INSTALLED = 1 << 0;
/**
* Flag to indicate whether the client app with the same long version code ({@link
* PackageInfo#getLongVersionCode} is installed in the occupant zone. If it's not installed,
* the caller may show a Dialog to promote the user to install or update the app. To get
* detailed package info of the client app, the caller can call {@link #getEndpointPackageInfo}.
*/
public static final int FLAG_CLIENT_SAME_LONG_VERSION = 1 << 1;
/**
* Flag to indicate whether the client app with the same signing info ({@link
* PackageInfo#signingInfo} is installed in the occupant zone. If it's not installed, the caller
* may show a Dialog to promote the user to install or update the app. To get detailed
* package info of the client app, the caller can call {@link #getEndpointPackageInfo}.
*/
public static final int FLAG_CLIENT_SAME_SIGNATURE = 1 << 2;
/**
* Flag to indicate whether the client app in the occupant zone is running. If it's not running,
* the caller may show a Dialog to promote the user to start the app.
*/
public static final int FLAG_CLIENT_RUNNING = 1 << 3;
/**
* Flag to indicate whether the client app in the occupant zone is running in the foreground
* (vs background). If UI is needed, the caller shouldn't request a connection to the client app
* when the client app is running in the background.
*/
public static final int FLAG_CLIENT_IN_FOREGROUND = 1 << 4;
/**
* Flags for the state of the occupant zone.
*
* @hide
*/
@IntDef(flag = true, prefix = {"FLAG_OCCUPANT_ZONE_"}, value = {
FLAG_OCCUPANT_ZONE_POWER_ON,
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED,
FLAG_OCCUPANT_ZONE_CONNECTION_READY,
})
@Retention(RetentionPolicy.SOURCE)
public @interface OccupantZoneState {
}
/**
* Flags for the state of client app in the occupant zone.
*
* @hide
*/
@IntDef(flag = true, prefix = {"FLAG_CLIENT_"}, value = {
FLAG_CLIENT_INSTALLED,
FLAG_CLIENT_SAME_LONG_VERSION,
FLAG_CLIENT_SAME_SIGNATURE,
FLAG_CLIENT_RUNNING,
FLAG_CLIENT_IN_FOREGROUND
})
@Retention(RetentionPolicy.SOURCE)
public @interface AppState {
}
/**
* A callback to allow the client to monitor other occupant zones in the car and peer clients
* in those occupant zones.
* <p>
* The caller can call {@link
* android.car.occupantconnection.CarOccupantConnectionManager#requestConnection} to connect to
* its peer client once the state of the peer occupant zone is {@link
* #FLAG_OCCUPANT_ZONE_CONNECTION_READY} and the state of the peer client becomes {@link
* android.car.CarRemoteDeviceManager#FLAG_CLIENT_INSTALLED}. If UI is needed to establish
* the connection, the caller must wait until {@link
* android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED} and {@link
* android.car.CarRemoteDeviceManager#FLAG_CLIENT_IN_FOREGROUND}) before requesting a
* connection.
*/
public interface StateCallback {
/**
* Invoked when the callback is registered, or when the {@link OccupantZoneState} of the
* occupant zone has changed.
*
* @param occupantZoneStates the state of the occupant zone. Multiple flags can be set in
* the state.
*/
void onOccupantZoneStateChanged(@NonNull OccupantZoneInfo occupantZone,
@OccupantZoneState int occupantZoneStates);
/**
* Invoked when the callback is registered, or when the {@link AppState} of the peer app in
* the given occupant zone has changed.
* <p>
* Note: Apps sharing the same user ID through the "sharedUserId" mechanism won't get
* notified when the running state of their peer apps has changed.
*
* @param appStates the state of the peer app. Multiple flags can be set in the state.
*/
void onAppStateChanged(@NonNull OccupantZoneInfo occupantZone,
@AppState int appStates);
}
private final Object mLock = new Object();
private final ICarRemoteDevice mService;
private final String mPackageName;
@GuardedBy("mLock")
private StateCallback mCallback;
@GuardedBy("mLock")
private Executor mCallbackExecutor;
private final IStateCallback mBinderCallback = new IStateCallback.Stub() {
@Override
public void onOccupantZoneStateChanged(OccupantZoneInfo occupantZone,
int occupantZoneStates) {
StateCallback callback;
Executor callbackExecutor;
synchronized (CarRemoteDeviceManager.this.mLock) {
callback = mCallback;
callbackExecutor = mCallbackExecutor;
}
if (callback == null || callbackExecutor == null) {
// This should never happen, but let's be cautious.
Slogf.e(TAG, "Failed to notify occupant zone state change because "
+ "the callback was unregistered! callback: " + callback
+ ", callbackExecutor: " + callbackExecutor);
return;
}
long token = Binder.clearCallingIdentity();
try {
callbackExecutor.execute(() -> callback.onOccupantZoneStateChanged(
occupantZone, occupantZoneStates));
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public void onAppStateChanged(OccupantZoneInfo occupantZone, int appStates) {
StateCallback callback;
Executor callbackExecutor;
synchronized (CarRemoteDeviceManager.this.mLock) {
callback = mCallback;
callbackExecutor = mCallbackExecutor;
}
if (callback == null || callbackExecutor == null) {
// This should never happen, but let's be cautious.
Slogf.e(TAG, "Failed to notify app state change because the "
+ "callback was unregistered! callback: " + callback
+ ", callbackExecutor: " + callbackExecutor);
return;
}
long token = Binder.clearCallingIdentity();
try {
callbackExecutor.execute(() ->
callback.onAppStateChanged(occupantZone, appStates));
} finally {
Binder.restoreCallingIdentity(token);
}
}
};
/** @hide */
public CarRemoteDeviceManager(Car car, IBinder service) {
super(car);
mService = ICarRemoteDevice.Stub.asInterface(service);
mPackageName = mCar.getContext().getPackageName();
}
/** @hide */
@Override
public void onCarDisconnected() {
synchronized (mLock) {
mCallback = null;
mCallbackExecutor = null;
}
}
/**
* Registers the {@code callback} to monitor the states of other occupant zones in the car and
* the peer clients in those occupant zones.
* <p>
* The client app can only register one {@link StateCallback}.
* The client app should call this method before requesting connection to its peer clients in
* other occupant zones.
*
* @param executor the Executor to run the callback
* @throws IllegalStateException if this client already registered a {@link StateCallback}
*/
@RequiresPermission(Car.PERMISSION_MANAGE_REMOTE_DEVICE)
public void registerStateCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull StateCallback callback) {
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
synchronized (mLock) {
Preconditions.checkState(mCallback == null,
"A StateCallback was registered already");
try {
mService.registerStateCallback(mPackageName, mBinderCallback);
mCallback = callback;
mCallbackExecutor = executor;
} catch (RemoteException e) {
Slogf.e(TAG, "Failed to register StateCallback");
handleRemoteExceptionFromCarService(e);
}
}
}
/**
* Unregisters the existing {@link StateCallback}.
* <p>
* This method can be called after calling {@link #registerStateCallback}, as soon
* as this caller no longer needs to monitor other occupant zones or becomes inactive.
* After monitoring ends, established connections won't be affected. In other words, {@link
* android.car.occupantconnection.Payload} can still be sent.
*
* @throws IllegalStateException if no {@link StateCallback} was registered by
* this {@link CarRemoteDeviceManager} before
*/
@RequiresPermission(Car.PERMISSION_MANAGE_REMOTE_DEVICE)
public void unregisterStateCallback() {
synchronized (mLock) {
Preconditions.checkState(mCallback != null, "There is no StateCallback "
+ "registered by this CarRemoteDeviceManager");
mCallback = null;
mCallbackExecutor = null;
try {
mService.unregisterStateCallback(mPackageName);
} catch (RemoteException e) {
Slogf.e(TAG, "Failed to unregister StateCallback");
handleRemoteExceptionFromCarService(e);
}
}
}
/**
* Returns the {@link PackageInfo} of the client in {@code occupantZone}, or
* {@code null} if there is no such client or an error occurred.
*/
@RequiresPermission(Car.PERMISSION_MANAGE_REMOTE_DEVICE)
@Nullable
public PackageInfo getEndpointPackageInfo(@NonNull OccupantZoneInfo occupantZone) {
Objects.requireNonNull(occupantZone, "occupantZone cannot be null");
try {
return mService.getEndpointPackageInfo(occupantZone.zoneId, mPackageName);
} catch (RemoteException e) {
Slogf.e(TAG, "Failed to get peer endpoint PackageInfo in " + occupantZone);
return handleRemoteExceptionFromCarService(e, null);
}
}
/**
* If {@code powerOn} is {@code true}, powers on all the displays of the given {@code
* occupantZone}, and powers on the associated Android system. If {@code powerOn} is
* {@code false}, powers off all the displays of given {@code occupantZone}, but doesn't
* power off the associated Android system.
* <p>
* It is not allowed to control the power of the driver occupant zone.
*
* @throws UnsupportedOperationException if {@code occupantZone} represents the driver occupant
* zone
*/
@RequiresPermission(allOf = {Car.PERMISSION_CAR_POWER, Car.PERMISSION_MANAGE_REMOTE_DEVICE})
public void setOccupantZonePower(@NonNull OccupantZoneInfo occupantZone, boolean powerOn) {
Objects.requireNonNull(occupantZone, "occupantZone cannot be null");
try {
mService.setOccupantZonePower(occupantZone, powerOn);
} catch (RemoteException e) {
Slogf.e(TAG, "Failed to control the power of " + occupantZone);
handleRemoteExceptionFromCarService(e);
}
}
/**
* Returns {@code true} if the associated Android system AND all the displays of the given
* {@code occupantZone} are powered on. Returns {@code false} otherwise.
*/
@RequiresPermission(Car.PERMISSION_MANAGE_REMOTE_DEVICE)
public boolean isOccupantZonePowerOn(@NonNull OccupantZoneInfo occupantZone) {
Objects.requireNonNull(occupantZone, "occupantZone cannot be null");
try {
return mService.isOccupantZonePowerOn(occupantZone);
} catch (RemoteException e) {
Slogf.e(TAG, "Failed to get power state of " + occupantZone);
return handleRemoteExceptionFromCarService(e, false);
}
}
}