blob: d368a312e3fb1d40b84e704b28179eada1489051 [file] [log] [blame]
/*
* Copyright (C) 2023 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 com.android.car.occupantconnection;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.car.CarOccupantZoneManager.INVALID_USER_ID;
import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_INSTALLED;
import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_IN_FOREGROUND;
import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_RUNNING;
import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_SAME_LONG_VERSION;
import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_SAME_SIGNATURE;
import static android.car.CarRemoteDeviceManager.FLAG_OCCUPANT_ZONE_CONNECTION_READY;
import static android.car.CarRemoteDeviceManager.FLAG_OCCUPANT_ZONE_POWER_ON;
import static android.car.CarRemoteDeviceManager.FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED;
import static android.car.builtin.display.DisplayManagerHelper.EVENT_FLAG_DISPLAY_CHANGED;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_INVISIBLE;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES;
import static com.android.car.CarServiceUtils.assertPermission;
import static com.android.car.CarServiceUtils.checkCalledByPackage;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEBUGGING_CODE;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
import static com.android.car.internal.util.VersionUtils.isPlatformVersionAtLeastU;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.RequiresApi;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.car.Car;
import android.car.CarOccupantZoneManager.OccupantZoneInfo;
import android.car.CarRemoteDeviceManager.AppState;
import android.car.CarRemoteDeviceManager.OccupantZoneState;
import android.car.builtin.app.ActivityManagerHelper.ProcessObserverCallback;
import android.car.builtin.display.DisplayManagerHelper;
import android.car.builtin.util.Slogf;
import android.car.occupantconnection.ICarRemoteDevice;
import android.car.occupantconnection.IStateCallback;
import android.car.user.CarUserManager;
import android.car.user.UserLifecycleEventFilter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Binder;
import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import com.android.car.CarLocalServices;
import com.android.car.CarOccupantZoneService;
import com.android.car.CarServiceBase;
import com.android.car.SystemActivityMonitoringService;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.BinderKeyValueContainer;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.car.power.CarPowerManagementService;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* Service to implement APIs defined in {@link android.car.CarRemoteDeviceManager}.
* <p>
* In this class, a discovering client refers to a client that has registered an {@link
* IStateCallback}, and discovered apps refer to the peer apps of the discovering client.
* <p>
* This class can monitor the states of occupant zones in the car and the peer clients in
* those occupant zones. There are 3 {@link OccupantZoneState}s:
* <ul>
* <li> {@link android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_POWER_ON} is updated by the
* DisplayListener.
* <li> TODO(b/257117236): implement FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED.
* <li> {@link android.car.CarRemoteDeviceManager#FLAG_OCCUPANT_ZONE_CONNECTION_READY} is updated
* by the ICarOccupantZoneCallback.
* </ul>
* There are 5 {@link AppState}s:
* <ul>
* <li> App install states ({@link android.car.CarRemoteDeviceManager#FLAG_CLIENT_INSTALLED},
* {@link android.car.CarRemoteDeviceManager#FLAG_CLIENT_SAME_LONG_VERSION},
* {@link android.car.CarRemoteDeviceManager#FLAG_CLIENT_SAME_SIGNATURE}) are updated by the
* PackageChangeReceiver.
* <li> App running states ({@link android.car.CarRemoteDeviceManager#FLAG_CLIENT_RUNNING},
* {@link android.car.CarRemoteDeviceManager#FLAG_CLIENT_IN_FOREGROUND}) are updated by the
* ProcessRunningStateCallback. Note: these states won't be updated for apps that share the
* same user ID through the "sharedUserId" mechanism.
* </ul>
*/
public class CarRemoteDeviceService extends ICarRemoteDevice.Stub implements
CarServiceBase {
private static final String TAG = CarRemoteDeviceService.class.getSimpleName();
private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);
private static final String INDENTATION_2 = " ";
private static final String INDENTATION_4 = " ";
private static final int PROCESS_NOT_RUNNING = 0;
private static final int PROCESS_RUNNING_IN_BACKGROUND = 1;
private static final int PROCESS_RUNNING_IN_FOREGROUND = 2;
@IntDef(flag = false, prefix = {"PROCESS_"}, value = {
PROCESS_NOT_RUNNING,
PROCESS_RUNNING_IN_BACKGROUND,
PROCESS_RUNNING_IN_FOREGROUND
})
@Retention(RetentionPolicy.SOURCE)
@interface ProcessRunningState {
}
@VisibleForTesting
@AppState
static final int INITIAL_APP_STATE = 0;
@VisibleForTesting
@OccupantZoneState
static final int INITIAL_OCCUPANT_ZONE_STATE = 0;
private final Object mLock = new Object();
private final Context mContext;
private final CarOccupantZoneService mOccupantZoneService;
private final CarPowerManagementService mPowerManagementService;
private final SystemActivityMonitoringService mSystemActivityMonitoringService;
private final ActivityManager mActivityManager;
private final UserManager mUserManager;
/** A map of discovering client to its callback. */
@GuardedBy("mLock")
private final BinderKeyValueContainer<ClientId, IStateCallback> mCallbackMap;
/** A map of client app to its {@link AppState}. */
@GuardedBy("mLock")
private final ArrayMap<ClientId, Integer> mAppStateMap;
/**
* A map of occupant zone to its {@link OccupantZoneState}. Its keys are all the occupant
* zones on this SoC and will never change after initialization.
*/
@GuardedBy("mLock")
private final ArrayMap<OccupantZoneInfo, Integer> mOccupantZoneStateMap;
/** A map of secondary user (non-system user) ID to PerUserInfo. */
@GuardedBy("mLock")
private final SparseArray<PerUserInfo> mPerUserInfoMap;
private final ProcessObserverCallback mProcessObserver = new ProcessObserver();
private final CarUserManager.UserLifecycleListener mUserLifecycleListener = event -> {
Slogf.v(TAG, "onEvent(%s)", event);
handleUserChange();
};
private final class PackageChangeReceiver extends BroadcastReceiver {
/** The user ID that this receiver registered as. */
private final int mUserId;
/** The occupant zone that the user runs in. */
private final OccupantZoneInfo mOccupantZone;
@VisibleForTesting
PackageChangeReceiver(int userId, OccupantZoneInfo occupantZone) {
super();
this.mUserId = userId;
this.mOccupantZone = occupantZone;
}
@Override
public void onReceive(Context context, Intent intent) {
String packageName = intent.getData().getSchemeSpecificPart();
synchronized (mLock) {
if (!isDiscoveringLocked(packageName)) {
// There is no peer client discovering this app, so ignore its install/uninstall
// event.
if (DBG) {
Slogf.v(TAG, "Ignore package change for %s as user %d because there is no "
+ "peer client discovering this app", packageName, mUserId);
}
return;
}
ClientId clientId = new ClientId(mOccupantZone, mUserId, packageName);
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
Slogf.v(TAG, "%s was installed", clientId);
@AppState int newState = calculateAppStateLocked(clientId);
setAppStateLocked(clientId, newState, /* callbackToNotify= */ null);
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
Slogf.v(TAG, "%s was uninstalled", clientId);
setAppStateLocked(clientId, INITIAL_APP_STATE, /* callbackToNotify= */ null);
}
}
}
}
private final class ProcessObserver extends ProcessObserverCallback {
@Override
public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
handleProcessRunningStateChange(uid, foregroundActivities
? PROCESS_RUNNING_IN_FOREGROUND
: PROCESS_RUNNING_IN_BACKGROUND);
}
@Override
public void onProcessDied(int pid, int uid) {
handleProcessRunningStateChange(uid, PROCESS_NOT_RUNNING);
}
}
/** Wrapper class for objects that are specific to a non-system user. */
@VisibleForTesting
static final class PerUserInfo {
/** The occupant zone that the user runs in. */
public final OccupantZoneInfo zone;
/** The Context of the user. Used to register and unregister the receiver. */
public final Context context;
/** The PackageManager of the user. */
public final PackageManager pm;
/** The PackageChangeReceiver. Used to listen to package install/uninstall events. */
public final BroadcastReceiver receiver;
@VisibleForTesting
PerUserInfo(OccupantZoneInfo zone, Context context, PackageManager pm,
BroadcastReceiver receiver) {
this.zone = zone;
this.context = context;
this.pm = pm;
this.receiver = receiver;
}
}
public CarRemoteDeviceService(Context context,
CarOccupantZoneService occupantZoneService,
CarPowerManagementService powerManagementService,
SystemActivityMonitoringService systemActivityMonitoringService) {
this(context, occupantZoneService, powerManagementService, systemActivityMonitoringService,
context.getSystemService(ActivityManager.class),
context.getSystemService(UserManager.class),
/* perUserInfoMap= */ new SparseArray<>(),
/* callbackMap= */ new BinderKeyValueContainer<>(),
/* appStateMap= */ new ArrayMap<>(),
/* occupantZoneStateMap= */ new ArrayMap<>());
}
@VisibleForTesting
CarRemoteDeviceService(Context context,
CarOccupantZoneService occupantZoneService,
CarPowerManagementService powerManagementService,
SystemActivityMonitoringService systemActivityMonitoringService,
ActivityManager activityManager,
UserManager userManager,
SparseArray<PerUserInfo> perUserInfoMap,
BinderKeyValueContainer<ClientId, IStateCallback> callbackMap,
ArrayMap<ClientId, Integer> appStateMap,
ArrayMap<OccupantZoneInfo, Integer> occupantZoneStateMap) {
mContext = context;
mOccupantZoneService = occupantZoneService;
mPowerManagementService = powerManagementService;
mSystemActivityMonitoringService = systemActivityMonitoringService;
mActivityManager = activityManager;
mUserManager = userManager;
mPerUserInfoMap = perUserInfoMap;
mCallbackMap = callbackMap;
mAppStateMap = appStateMap;
mOccupantZoneStateMap = occupantZoneStateMap;
}
@Override
public void init() {
if (!isPlatformVersionAtLeastU()) {
Slogf.w(TAG, "CarRemoteDeviceService should run on Android U+");
return;
}
initAllOccupantZones();
registerUserLifecycleListener();
initAssignedUsers();
registerDisplayListener();
}
@Override
public void release() {
// TODO(b/257117236): implement this method.
}
/** Run `adb shell dumpsys car_service --services CarRemoteDeviceService` to dump. */
@Override
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
public void dump(IndentingPrintWriter writer) {
writer.println("*CarRemoteDeviceService*");
synchronized (mLock) {
writer.printf("%smCallbackMap:\n", INDENTATION_2);
for (int i = 0; i < mCallbackMap.size(); i++) {
ClientId discoveringClient = mCallbackMap.keyAt(i);
IStateCallback callback = mCallbackMap.valueAt(i);
writer.printf("%s%s, callback:%s\n", INDENTATION_4, discoveringClient, callback);
}
writer.printf("%smAppStateMap:\n", INDENTATION_2);
for (int i = 0; i < mAppStateMap.size(); i++) {
ClientId client = mAppStateMap.keyAt(i);
@AppState int state = mAppStateMap.valueAt(i);
writer.printf("%s%s, state:%s\n", INDENTATION_4, client, appStateToString(state));
}
writer.printf("%smOccupantZoneStateMap:\n", INDENTATION_2);
for (int i = 0; i < mOccupantZoneStateMap.size(); i++) {
OccupantZoneInfo occupantZone = mOccupantZoneStateMap.keyAt(i);
@OccupantZoneState int state = mOccupantZoneStateMap.valueAt(i);
writer.printf("%s%s, state:%s\n", INDENTATION_4, occupantZone,
occupantZoneStateToString(state));
}
writer.printf("%smPerUserInfoMap:\n", INDENTATION_2);
for (int i = 0; i < mPerUserInfoMap.size(); i++) {
int userId = mPerUserInfoMap.keyAt(i);
PerUserInfo info = mPerUserInfoMap.valueAt(i);
writer.printf("%suserId:%s, %s, %s, %s, %s\n", INDENTATION_4, userId, info.zone,
info.context, info.pm, info.receiver);
}
}
}
@Override
public void registerStateCallback(String packageName, IStateCallback callback) {
assertPermission(mContext, Car.PERMISSION_MANAGE_REMOTE_DEVICE);
checkCalledByPackage(mContext, packageName);
ClientId discoveringClient = getCallingClientId(packageName);
synchronized (mLock) {
assertNoDuplicateCallbackLock(discoveringClient);
boolean firstDiscoverer = mCallbackMap.size() == 0;
mCallbackMap.put(discoveringClient, callback);
// Notify the discoverer of the latest states.
updateAllOccupantZoneStateLocked(callback);
updateAllAppStateWithPackageNameLocked(discoveringClient.packageName, callback);
if (firstDiscoverer) {
mSystemActivityMonitoringService.registerProcessObserverCallback(mProcessObserver);
}
}
}
@Override
public void unregisterStateCallback(String packageName) {
assertPermission(mContext, Car.PERMISSION_MANAGE_REMOTE_DEVICE);
checkCalledByPackage(mContext, packageName);
ClientId discoveringClient = getCallingClientId(packageName);
synchronized (mLock) {
assertHasCallbackLock(discoveringClient);
mCallbackMap.remove(discoveringClient);
if (mCallbackMap.size() == 0) {
mSystemActivityMonitoringService.unregisterProcessObserverCallback(
mProcessObserver);
}
// If this discoverer is the last discoverer with the package name, remove the app state
// of all the apps with the package name.
if (!isDiscoveringLocked(packageName)) {
clearAllAppStateWithPackageNameLocked(packageName);
}
}
}
@Override
public PackageInfo getEndpointPackageInfo(int occupantZoneId, String packageName) {
assertPermission(mContext, Car.PERMISSION_MANAGE_REMOTE_DEVICE);
checkCalledByPackage(mContext, packageName);
int userId = mOccupantZoneService.getUserForOccupant(occupantZoneId);
if (userId == INVALID_USER_ID) {
Slogf.e(TAG, "Failed to get PackageInfo of %s in occupant zone %d because it has no "
+ "user assigned", packageName, occupantZoneId);
return null;
}
return getPackageInfoAsUser(packageName, userId);
}
@Override
public void setOccupantZonePower(OccupantZoneInfo occupantZone, boolean powerOn) {
assertPermission(mContext, Car.PERMISSION_MANAGE_REMOTE_DEVICE);
int[] displayIds = mOccupantZoneService.getAllDisplaysForOccupantZone(occupantZone.zoneId);
for (int id : displayIds) {
mPowerManagementService.setDisplayPowerState(id, powerOn);
}
}
@Override
public boolean isOccupantZonePowerOn(OccupantZoneInfo occupantZone) {
assertPermission(mContext, Car.PERMISSION_MANAGE_REMOTE_DEVICE);
return mOccupantZoneService.areDisplaysOnForOccupantZone(occupantZone.zoneId);
}
private void initAllOccupantZones() {
List<OccupantZoneInfo> allOccupantZones = mOccupantZoneService.getAllOccupantZones();
synchronized (mLock) {
for (int i = 0; i < allOccupantZones.size(); i++) {
OccupantZoneInfo occupantZone = allOccupantZones.get(i);
@OccupantZoneState int initialState = calculateOccupantZoneState(occupantZone);
Slogf.v(TAG, "The state of %s is initialized to %s", occupantZone,
occupantZoneStateToString(initialState));
mOccupantZoneStateMap.put(occupantZone, initialState);
}
}
}
private void registerUserLifecycleListener() {
CarUserService userService = CarLocalServices.getService(CarUserService.class);
UserLifecycleEventFilter userEventFilter = new UserLifecycleEventFilter.Builder()
// UNLOCKED event indicates the connection becomes ready, while INVISIBLE event
// indicates the connection changes to not ready.
.addEventType(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
.addEventType(USER_LIFECYCLE_EVENT_TYPE_INVISIBLE)
.build();
userService.addUserLifecycleListener(userEventFilter, mUserLifecycleListener);
}
/**
* Handles the user change in all the occpant zones, including the driver occupant zone and
* passenger occupant zones.
*/
private void handleUserChange() {
synchronized (mLock) {
for (int i = 0; i < mOccupantZoneStateMap.size(); i++) {
OccupantZoneInfo occupantZone = mOccupantZoneStateMap.keyAt(i);
int oldUserId = getAssignedUserLocked(occupantZone);
int newUserId =
mOccupantZoneService.getUserForOccupant(occupantZone.zoneId);
Slogf.i(TAG, "In %s, old user was %d, new user is %d",
occupantZone, oldUserId, newUserId);
boolean hasOldUser = (oldUserId != INVALID_USER_ID);
boolean hasNewUser = isNonSystemUser(newUserId);
if (!hasOldUser && !hasNewUser) {
// Still no user secondary assigned in this occupant zone, so do nothing.
Slogf.v(TAG, "Still no user secondary assigned in %s", occupantZone);
continue;
}
if (oldUserId == newUserId) {
// The user ID doesn't change in this occupant zone, but the user lifecycle
// might have changed, so try to update the occupant zone state.
handleSameUserUpdateLocked(newUserId, occupantZone);
continue;
}
if (hasOldUser && !hasNewUser) {
// The old user was unassigned.
handleUserUnassignedLocked(oldUserId, occupantZone);
continue;
}
if (!hasOldUser && hasNewUser) {
// The new user was assigned.
handleUserAssignedLocked(newUserId, occupantZone);
continue;
}
// The old user switched to a different new user.
handleUserSwitchedLocked(oldUserId, newUserId, occupantZone);
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private void registerDisplayListener() {
DisplayManagerHelper.registerDisplayListener(mContext, new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
// No-op.
}
@Override
public void onDisplayRemoved(int displayId) {
// No-op.
}
@Override
public void onDisplayChanged(int displayId) {
Slogf.v(TAG, "onDisplayChanged(): displayId %d", displayId);
OccupantZoneInfo occupantZone =
mOccupantZoneService.getOccupantZoneForDisplayId(displayId);
if (occupantZone == null) {
Slogf.i(TAG, "Display %d has no occupant zone assigned", displayId);
return;
}
synchronized (mLock) {
updateOccupantZoneStateLocked(occupantZone,
/* callbackToNotify= */null);
}
}
}, /* handler= */null, EVENT_FLAG_DISPLAY_CHANGED);
}
private void handleProcessRunningStateChange(int uid, @ProcessRunningState int newState) {
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
if (userHandle.isSystem()) {
if (DBG) {
Slogf.v(TAG, "Skip ProcessRunningState change for process with uid %d because "
+ "the process runs as system user", uid);
}
return;
}
synchronized (mLock) {
String packageName = getUniquePackageNameByUidLocked(uid);
if (packageName == null) {
return;
}
if (!isDiscoveringLocked(packageName)) {
// There is no peer client discovering this app, so ignore its running state change
// event.
if (DBG) {
Slogf.v(TAG, "Skip ProcessRunningState change for %s because there is no peer "
+ "client discovering this app", packageName);
}
return;
}
Slogf.v(TAG, "%s 's running state changed to %s", packageName,
processRunningStateToString(newState));
int userId = userHandle.getIdentifier();
// Note: userInfo can't be null here, otherwise getUniquePackageNameByUidLocked() would
// return null, and it wouldn't get here.
PerUserInfo userInfo = mPerUserInfoMap.get(userId);
ClientId clientId = new ClientId(userInfo.zone, userId, packageName);
@AppState int newAppState =
convertProcessRunningStateToAppStateLocked(packageName, userId, newState);
setAppStateLocked(clientId, newAppState, /* callbackToNotify= */ null);
}
}
private void initAssignedUsers() {
synchronized (mLock) {
for (int i = 0; i < mOccupantZoneStateMap.size(); i++) {
OccupantZoneInfo occupantZone = mOccupantZoneStateMap.keyAt(i);
int userId = mOccupantZoneService.getUserForOccupant(occupantZone.zoneId);
Slogf.v(TAG, "User ID of %s is %d ", occupantZone, userId);
if (!isNonSystemUser(userId)) {
continue;
}
initAssignedUserLocked(userId, occupantZone);
}
}
}
/**
* Initializes PerUserInfo for the given user, and registers a PackageChangeReceiver for the
* given user.
*
* @return {@code true} if the PerUserInfo was initialized successfully
*/
@GuardedBy("mLock")
private boolean initAssignedUserLocked(int userId, OccupantZoneInfo occupantZone) {
if (!isNonSystemUser(userId)) {
Slogf.w(TAG, "%s is assigned to user %d", occupantZone, userId);
return false;
}
PerUserInfo userInfo = mPerUserInfoMap.get(userId);
if (userInfo != null && userInfo.zone.equals(occupantZone)) {
Slogf.v(TAG, "Skip initializing PerUserInfo of user %d because it exists already",
userId);
return true;
}
// Init PackageChangeReceiver.
PackageChangeReceiver receiver = new PackageChangeReceiver(userId, occupantZone);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
// Init user Context.
Context userContext = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
if (userContext == null) {
Slogf.e(TAG, "Failed to create Context as user %d", userId);
return false;
}
Slogf.v(TAG, "registerReceiver() as user %d", userId);
// Register PackageChangeReceiver.
userContext.registerReceiver(receiver, filter);
// Init PackageManager.
PackageManager pm = userContext.getPackageManager();
if (pm == null) {
Slogf.e(TAG, "Failed to create PackageManager as user %d", userId);
return false;
}
userInfo = new PerUserInfo(occupantZone, userContext, pm, receiver);
mPerUserInfoMap.put(userId, userInfo);
return true;
}
/**
* Removes PerUserInfo of the given user, and unregisters the PackageChangeReceiver for the
* given user. This method is called when the given {@code userId} is unassigned.
*/
@GuardedBy("mLock")
private void removeUnassignedUserLocked(int userId) {
PerUserInfo userInfo = mPerUserInfoMap.get(userId);
if (userInfo == null) {
Slogf.v(TAG, "Skip removing PerUserInfo of user %d because it doesn't exist", userId);
return;
}
Slogf.v(TAG, "unregisterReceiver() as user %d", userId);
userInfo.context.unregisterReceiver(userInfo.receiver);
mPerUserInfoMap.remove(userId);
}
@GuardedBy("mLock")
private void handleSameUserUpdateLocked(int userId, OccupantZoneInfo occupantZone) {
Slogf.v(TAG, "User %d lifecycle might have changed in %s", userId, occupantZone);
updateOccupantZoneStateLocked(occupantZone, /* callbackToNotify= */ null);
}
@GuardedBy("mLock")
private void handleUserUnassignedLocked(int userId, OccupantZoneInfo occupantZone) {
Slogf.v(TAG, "User %d was unassigned in %s", userId, occupantZone);
removeUnassignedUserLocked(userId);
updateOccupantZoneStateLocked(occupantZone, /* callbackToNotify= */ null);
clearAllAppStateAsUserLocked(userId);
}
@GuardedBy("mLock")
private void handleUserAssignedLocked(int userId, OccupantZoneInfo occupantZone) {
Slogf.v(TAG, "User %d was assigned in %s", userId, occupantZone);
initAssignedUserLocked(userId, occupantZone);
updateOccupantZoneStateLocked(occupantZone, /* callbackToNotify= */ null);
updateAllAppStateForNewUserLocked(userId, occupantZone);
}
@GuardedBy("mLock")
private void handleUserSwitchedLocked(int oldUserId, int newUserId,
OccupantZoneInfo occupantZone) {
Slogf.v(TAG, "User %d was switched to %d in %s", oldUserId, newUserId, occupantZone);
removeUnassignedUserLocked(oldUserId);
clearAllAppStateAsUserLocked(oldUserId);
initAssignedUserLocked(newUserId, occupantZone);
updateAllAppStateForNewUserLocked(newUserId, occupantZone);
updateOccupantZoneStateLocked(occupantZone, /* callbackToNotify= */ null);
}
private ClientId getCallingClientId(String packageName) {
UserHandle callingUserHandle = Binder.getCallingUserHandle();
int callingUserId = callingUserHandle.getIdentifier();
OccupantZoneInfo occupantZone =
mOccupantZoneService.getOccupantZoneForUser(callingUserHandle);
// Note: the occupantZone is not null because the calling user must be a valid user.
return new ClientId(occupantZone, callingUserId, packageName);
}
/**
* Updates the states of all the occupant zones, notifies the newly registered callback
* {@code callbackToNotify} of the latest state if it is not {@code null}, and notifies other
* callbacks of the latest state if the state has changed.
*/
@GuardedBy("mLock")
private void updateAllOccupantZoneStateLocked(@Nullable IStateCallback callbackToNotify) {
for (int i = 0; i < mOccupantZoneStateMap.size(); i++) {
OccupantZoneInfo occupantZone = mOccupantZoneStateMap.keyAt(i);
updateOccupantZoneStateLocked(occupantZone, callbackToNotify);
}
}
/**
* Updates the state of the given occupant zone, notifies the newly registered callback
* {@code callbackToNotify} of the latest state if it is not {@code null}, and notifies other
* callbacks of the latest state if the state has changed.
*/
@GuardedBy("mLock")
private void updateOccupantZoneStateLocked(OccupantZoneInfo occupantZone,
@Nullable IStateCallback callbackToNotify) {
@OccupantZoneState int oldState = mOccupantZoneStateMap.get(occupantZone);
@OccupantZoneState int newState = calculateOccupantZoneState(occupantZone);
boolean stateChanged = (oldState != newState);
if (!stateChanged && callbackToNotify == null) {
Slogf.v(TAG, "Skip updateOccupantZoneStateLocked() for %s because OccupantZoneState"
+ " stays the same and there is no newly registered callback", occupantZone);
return;
}
Slogf.v(TAG, "The state of %s is changed from %s to %s", occupantZone,
occupantZoneStateToString(oldState), occupantZoneStateToString(newState));
mOccupantZoneStateMap.put(occupantZone, newState);
for (int i = 0; i < mCallbackMap.size(); i++) {
ClientId discoveringClient = mCallbackMap.keyAt(i);
// Don't notify discovering clients that are running in this occupant zone.
if (discoveringClient.occupantZone.equals(occupantZone)) {
continue;
}
IStateCallback callback = mCallbackMap.valueAt(i);
// If the callback is newly registered, invoke it anyway. Otherwise, invoke it only
// when the state has changed
if ((callback == callbackToNotify) || stateChanged) {
try {
callback.onOccupantZoneStateChanged(occupantZone, newState);
} catch (RemoteException e) {
Slogf.e(TAG, e, "Failed to notify %s of OccupantZoneState change",
discoveringClient);
}
}
}
}
@VisibleForTesting
@OccupantZoneState
int calculateOccupantZoneState(OccupantZoneInfo occupantZone) {
@OccupantZoneState int occupantZoneState = INITIAL_OCCUPANT_ZONE_STATE;
// The three occupant zones states are independent of each other.
if (isPowerOn(occupantZone)) {
occupantZoneState |= FLAG_OCCUPANT_ZONE_POWER_ON;
}
if (isScreenUnlocked(occupantZone)) {
occupantZoneState |= FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED;
}
if (isConnectionReady(occupantZone)) {
occupantZoneState |= FLAG_OCCUPANT_ZONE_CONNECTION_READY;
}
return occupantZoneState;
}
private boolean isPowerOn(OccupantZoneInfo occupantZone) {
return mOccupantZoneService.areDisplaysOnForOccupantZone(occupantZone.zoneId);
}
private boolean isScreenUnlocked(OccupantZoneInfo occupantZone) {
// TODO(b/257117236): implement this method.
return false;
}
/**
* Returns {@code true} if the given {@code occupantZone} is ready to handle connection request.
* Returns {@code false} otherwise.
* <p>
* If the {@code occupantZone} is on the same SoC as the caller occupant zone, connection ready
* means the user is ready. If the {@code occupantZone} is on another SoC, connection ready
* means user ready and internet connection from the caller occupant zone to the {@code
* occupantZone} is good. User ready means the user has been allocated to the occupant zone,
* is actively running, is unlocked, and is visible.
*/
// TODO(b/257118327): support multi-SoC.
boolean isConnectionReady(OccupantZoneInfo occupantZone) {
if (!isPlatformVersionAtLeastU()) {
Slogf.w(TAG, "CarRemoteDeviceService should run on Android U+");
return false;
}
int userId = mOccupantZoneService.getUserForOccupant(occupantZone.zoneId);
if (!isNonSystemUser(userId)) {
return false;
}
UserHandle userHandle = UserHandle.of(userId);
return mUserManager.isUserRunning(userHandle) && mUserManager.isUserUnlocked(userHandle)
&& mUserManager.getVisibleUsers().contains(userHandle);
}
/**
* Updates the {@link AppState} of all the apps with the given {@code packageName}, notifies
* the newly registered callback {@code callbackToNotify} of the latest state, and notifies
* other callbacks of the latest state if the state has changed.
*/
@GuardedBy("mLock")
private void updateAllAppStateWithPackageNameLocked(String packageName,
IStateCallback callbackToNotify) {
for (int i = 0; i < mPerUserInfoMap.size(); i++) {
int userId = mPerUserInfoMap.keyAt(i);
OccupantZoneInfo occupantZone = mPerUserInfoMap.valueAt(i).zone;
ClientId discoveredClient = new ClientId(occupantZone, userId, packageName);
@AppState int newState = calculateAppStateLocked(discoveredClient);
setAppStateLocked(discoveredClient, newState, callbackToNotify);
}
}
/**
* Updates the {@link AppState} of all the clients that run as {@code userId}, and notifies
* the discoverers of the state change. This method is invoked when a new user is assigned to
* the given occupant zone.
*/
@GuardedBy("mLock")
private void updateAllAppStateForNewUserLocked(int userId, OccupantZoneInfo occupantZone) {
Set<String> updatedApps = new ArraySet<>();
for (int i = 0; i < mCallbackMap.size(); i++) {
ClientId discoveringClient = mCallbackMap.keyAt(i);
// For a given package name, there might be several discoverers (peer clients that have
// registered a callback), but we only need to update the state of the changed client
// once.
if (updatedApps.contains(discoveringClient.packageName)) {
continue;
}
updatedApps.add(discoveringClient.packageName);
ClientId clientId = new ClientId(occupantZone, userId, discoveringClient.packageName);
@AppState int newAppState = calculateAppStateLocked(clientId);
setAppStateLocked(clientId, newAppState, /* callbackToNotify= */ null);
}
}
/**
* Clears the {@link AppState} of all the apps that run as {@code userId}.
* This method is called when the given {@code userId} is unassigned for the occupantZone,
* for which the discoverers are already notified, so there is no need to notify the discoverers
* in this method.
*/
@GuardedBy("mLock")
private void clearAllAppStateAsUserLocked(int userId) {
for (int i = 0; i < mAppStateMap.size(); i++) {
ClientId clientId = mAppStateMap.keyAt(i);
if (clientId.userId == userId) {
mAppStateMap.removeAt(i);
}
}
}
/**
* Clears the {@link AppState} of all the apps that have the given {@code packageName}.
* This method is called when the last discoverer with the package name is unregistered , so
* there is no need to notify the discoverers in this method.
*/
@GuardedBy("mLock")
private void clearAllAppStateWithPackageNameLocked(String packageName) {
for (int i = 0; i < mAppStateMap.size(); i++) {
ClientId clientId = mAppStateMap.keyAt(i);
if (clientId.packageName.equals(packageName)) {
mAppStateMap.removeAt(i);
}
}
}
@GuardedBy("mLock")
private PackageManager getPackageManagerAsUserLocked(int userId) {
PerUserInfo userInfo = mPerUserInfoMap.get(userId);
if (userInfo == null) {
Slogf.e(TAG, "Failed to get PackageManager as user %d because the user is not"
+ " assigned to an occupant zone yet", userId);
return null;
}
return userInfo.pm;
}
@GuardedBy("mLock")
private void assertNoDuplicateCallbackLock(ClientId discoveredClient) {
if (mCallbackMap.containsKey(discoveredClient)) {
throw new IllegalStateException("The client already registered a StateCallback: "
+ discoveredClient);
}
}
@GuardedBy("mLock")
private void assertHasCallbackLock(ClientId discoveredClient) {
if (!mCallbackMap.containsKey(discoveredClient)) {
throw new IllegalStateException("The client has no StateCallback registered: "
+ discoveredClient);
}
}
/**
* Returns {@code true} if there is a client with the {@code packageName} has registered an
* {@link IStateCallback}.
*/
@GuardedBy("mLock")
private boolean isDiscoveringLocked(String packageName) {
for (int i = 0; i < mCallbackMap.size(); i++) {
ClientId discoveringClient = mCallbackMap.keyAt(i);
if (discoveringClient.packageName.equals(packageName)) {
return true;
}
}
return false;
}
/**
* Sets the {@link AppState} of the given client, notifies the newly registered callback
* {@code callbackToNotify} of the latest state if it is not {@code null}, and notifies other
* peer discoverers of the latest state if the state has changed.
*/
@GuardedBy("mLock")
private void setAppStateLocked(ClientId discoveredClient, @AppState int newState,
@Nullable IStateCallback callbackToNotify) {
Integer oldAppState = mAppStateMap.get(discoveredClient);
boolean stateChanged = (oldAppState == null || oldAppState.intValue() != newState);
if (!stateChanged && callbackToNotify == null) {
Slogf.v(TAG, "Skip setAppStateLocked() because AppState stays the same and there"
+ " is no newly registered callback");
return;
}
Slogf.v(TAG, "The app state of %s is set from %s to %s", discoveredClient,
oldAppState == null ? "null" : appStateToString(oldAppState),
appStateToString(newState));
mAppStateMap.put(discoveredClient, newState);
// Notify its peer clients that are discovering.
for (int i = 0; i < mCallbackMap.size(); i++) {
ClientId discoveringClient = mCallbackMap.keyAt(i);
// A peer client is a client that has the same package name but runs as another user.
// If it is not a peer client, skip it.
if (!discoveringClient.packageName.equals(discoveredClient.packageName)
|| discoveringClient.userId == discoveredClient.userId) {
continue;
}
IStateCallback callback = mCallbackMap.valueAt(i);
// If the callback is newly registered, invoke it anyway. Otherwise, invoke it only
// when the state has changed
if (callback == callbackToNotify || stateChanged) {
try {
callback.onAppStateChanged(discoveredClient.occupantZone, newState);
} catch (RemoteException e) {
Slogf.e(TAG, e, "Failed to notify %d of AppState change", discoveringClient);
}
}
}
}
@GuardedBy("mLock")
@AppState
private int calculateAppStateLocked(ClientId clientId) {
@AppState int appState = INITIAL_APP_STATE;
if (isAppInstalledAsUserLocked(clientId.packageName, clientId.userId)) {
appState |= FLAG_CLIENT_INSTALLED;
// In single-SoC model, the peer client is guaranteed to have the same
// signing info and long version code.
// TODO(b/257118327): support multiple-SoC.
appState |= FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE;
RunningAppProcessInfo info =
getRunningAppProcessInfoAsUserLocked(clientId.packageName, clientId.userId);
if (isAppRunning(info)) {
appState |= FLAG_CLIENT_RUNNING;
if (isAppRunningInForeground(info)) {
appState |= FLAG_CLIENT_IN_FOREGROUND;
}
}
}
return appState;
}
@GuardedBy("mLock")
private int getAssignedUserLocked(OccupantZoneInfo occupantZone) {
for (int i = 0; i < mPerUserInfoMap.size(); i++) {
if (occupantZone.equals(mPerUserInfoMap.valueAt(i).zone)) {
return mPerUserInfoMap.keyAt(i);
}
}
return INVALID_USER_ID;
}
/**
* This method is an unlocked version of {@link #calculateAppStateLocked} and is used for
* testing only.
*/
@AppState
@VisibleForTesting
int calculateAppState(ClientId clientId) {
synchronized (mLock) {
return calculateAppStateLocked(clientId);
}
}
@GuardedBy("mLock")
private boolean isAppInstalledAsUserLocked(String packageName, int userId) {
return getPackageInfoAsUserLocked(packageName, userId, /* flags= */ 0) != null;
}
PackageInfo getPackageInfoAsUser(String packageName, int userId) {
synchronized (mLock) {
return getPackageInfoAsUserLocked(packageName, userId, GET_SIGNING_CERTIFICATES);
}
}
@GuardedBy("mLock")
private PackageInfo getPackageInfoAsUserLocked(String packageName, int userId, int flags) {
PackageManager pm = getPackageManagerAsUserLocked(userId);
if (pm == null) {
return null;
}
try {
return pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags));
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
@GuardedBy("mLock")
private RunningAppProcessInfo getRunningAppProcessInfoAsUserLocked(String packageName,
int userId) {
List<RunningAppProcessInfo> infos = mActivityManager.getRunningAppProcesses();
if (infos == null) {
return null;
}
for (int i = 0; i < infos.size(); i++) {
RunningAppProcessInfo processInfo = infos.get(i);
if (processInfo.processName.equals(packageName)) {
UserHandle processUserHandle = UserHandle.getUserHandleForUid(processInfo.uid);
if (processUserHandle.getIdentifier() == userId) {
return processInfo;
}
}
}
return null;
}
@GuardedBy("mLock")
private String getUniquePackageNameByUidLocked(int uid) {
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
int userId = userHandle.getIdentifier();
PerUserInfo userInfo = mPerUserInfoMap.get(userId);
if (userInfo == null) {
// When an occupant zone is assigned with a user, the associated PerUserInfo will be
// initialized in the ICarOccupantZoneCallback. But the ICarOccupantZoneCallback may be
// invoked after this method (called by ProcessObserverCallback). In that case, the
// PerUserInfo will be null (b/277956688). So let's try to initialize the PerUserInfo
// here.
Slogf.v(TAG, "PerUserIno for user %d is not initialized yet", userId);
OccupantZoneInfo occupantZone = mOccupantZoneService.getOccupantZoneForUser(userHandle);
if (occupantZone == null) {
// This shouldn't happen. Let's log an error.
Slogf.e(TAG, "The running state of the process (uid %d) has changed, but the user"
+ " %d is not assigned to any occupant zone yet", uid, userId);
return null;
}
boolean success = initAssignedUserLocked(userId, occupantZone);
if (!success) {
Slogf.wtf(TAG, "Failed to initialize PerUserInfo for user %d in %s", userId,
occupantZone);
return null;
}
// Note: userInfo must not be null here because it was initialized successfully.
userInfo = mPerUserInfoMap.get(userId);
}
String[] packageNames = userInfo.pm.getPackagesForUid(uid);
if (packageNames == null) {
return null;
}
if (packageNames.length == 1) {
return packageNames[0];
}
// packageNames.length can't be 0.
// Multiple package names means multiple apps share the same user ID through the
// "sharedUserId" mechanism. However, "sharedUserId" mechanism is deprecated in
// API level 29, so let's log an error.
Slogf.i(TAG, "Failed to get the package name by uid! Apps shouldn't use sharedUserId"
+ " because it's deprecated in API level 29: %s", Arrays.toString(packageNames));
return null;
}
@GuardedBy("mLock")
@AppState
private int convertProcessRunningStateToAppStateLocked(String packageName, int userId,
@ProcessRunningState int state) {
// Note: In single-SoC model, the peer client is guaranteed to have the same
// signing info and long version code.
// TODO(b/257118327): support multiple-SoC.
switch (state) {
case PROCESS_RUNNING_IN_BACKGROUND:
return FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION
| FLAG_CLIENT_SAME_SIGNATURE | FLAG_CLIENT_RUNNING;
case PROCESS_RUNNING_IN_FOREGROUND:
return FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION
| FLAG_CLIENT_SAME_SIGNATURE | FLAG_CLIENT_RUNNING
| FLAG_CLIENT_IN_FOREGROUND;
case PROCESS_NOT_RUNNING:
return isAppInstalledAsUserLocked(packageName, userId)
? FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION
| FLAG_CLIENT_SAME_SIGNATURE
: INITIAL_APP_STATE;
}
throw new IllegalArgumentException("Undefined ProcessRunningState: " + state);
}
private static boolean isAppRunning(RunningAppProcessInfo info) {
return info != null;
}
private static boolean isAppRunningInForeground(RunningAppProcessInfo info) {
return info != null && info.importance == IMPORTANCE_FOREGROUND;
}
/** Returns {@code true} if the given user is a valid user and is not the system user. */
private static boolean isNonSystemUser(int userId) {
return userId != INVALID_USER_ID && !UserHandle.of(userId).isSystem();
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DEBUGGING_CODE)
private static String occupantZoneStateToString(@OccupantZoneState int state) {
boolean powerOn = (state & FLAG_OCCUPANT_ZONE_POWER_ON) != 0;
boolean screenUnlocked = (state & FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED) != 0;
boolean connectionReady = (state & FLAG_OCCUPANT_ZONE_CONNECTION_READY) != 0;
return new StringBuilder(64)
.append("[")
.append(powerOn ? "on, " : "off, ")
.append(screenUnlocked ? "unlocked, " : "locked, ")
.append(connectionReady ? "ready" : "not-ready")
.append("]")
.toString();
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DEBUGGING_CODE)
private static String appStateToString(@AppState int state) {
boolean installed = (state & FLAG_CLIENT_INSTALLED) != 0;
boolean sameVersion = (state & FLAG_CLIENT_SAME_LONG_VERSION) != 0;
boolean sameSignature = (state & FLAG_CLIENT_SAME_SIGNATURE) != 0;
boolean running = (state & FLAG_CLIENT_RUNNING) != 0;
boolean inForeground = (state & FLAG_CLIENT_IN_FOREGROUND) != 0;
return new StringBuilder(64)
.append("[")
.append(installed ? "installed, " : "not-installed, ")
.append(sameVersion ? "same-version, " : "different-version, ")
.append(sameSignature ? "same-signature, " : "different-signature, ")
.append(!running
? "not-running"
: (inForeground ? "foreground" : "background"))
.append("]")
.toString();
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DEBUGGING_CODE)
private static String processRunningStateToString(@ProcessRunningState int state) {
switch (state) {
case PROCESS_NOT_RUNNING:
return "not-running";
case PROCESS_RUNNING_IN_BACKGROUND:
return "background";
case PROCESS_RUNNING_IN_FOREGROUND:
return "foreground";
default:
throw new IllegalArgumentException("Undefined ProcessRunningState: " + state);
}
}
}