/* * 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. *
* 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. *
* 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. *
* 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. *
* 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}. *
* 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. *
* 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); } } }