blob: 7ba69291d6733bb4b09299a5dac9ed12383d9f27 [file] [log] [blame]
/*
* Copyright (C) 2019 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.car.Car;
import android.car.CarInfoManager;
import android.car.CarOccupantZoneManager;
import android.car.CarOccupantZoneManager.DisplayTypeEnum;
import android.car.CarOccupantZoneManager.OccupantTypeEnum;
import android.car.CarOccupantZoneManager.OccupantZoneInfo;
import android.car.ICarOccupantZone;
import android.car.ICarOccupantZoneCallback;
import android.car.VehicleAreaSeat;
import android.car.media.CarAudioManager;
import android.car.user.CarUserManager;
import android.car.user.CarUserManager.UserLifecycleListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.IntArray;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.Display;
import android.view.DisplayAddress;
import com.android.car.internal.ICarServiceHelper;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.utils.Slogf;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Service to implement CarOccupantZoneManager API.
*/
public final class CarOccupantZoneService extends ICarOccupantZone.Stub
implements CarServiceBase {
private static final String TAG = CarLog.tagFor(CarOccupantZoneService.class);
private static final String ALL_COMPONENTS = "*";
private static final int INVALID_PORT = -1;
private final Object mLock = new Object();
private final Context mContext;
private final DisplayManager mDisplayManager;
private final UserManager mUserManager;
private final boolean mEnableProfileUserAssignmentForMultiDisplay;
private boolean mEnableSourcePreferred;
private ArrayList<ComponentName> mSourcePreferredComponents;
/**
* Stores android user id of profile users for the current user.
*/
@GuardedBy("mLock")
private final ArraySet<Integer> mProfileUsers = new ArraySet<>();
/** key: zone id */
@GuardedBy("mLock")
private final SparseArray<OccupantZoneInfo> mOccupantsConfig = new SparseArray<>();
@VisibleForTesting
static class DisplayConfig {
public final int displayType;
public final int occupantZoneId;
DisplayConfig(int displayType, int occupantZoneId) {
this.displayType = displayType;
this.occupantZoneId = occupantZoneId;
}
@Override
public String toString() {
// do not include type as this is only used for dump
StringBuilder b = new StringBuilder(64);
b.append("{displayType=");
b.append(Integer.toHexString(displayType));
b.append(" occupantZoneId=");
b.append(occupantZoneId);
b.append("}");
return b.toString();
}
}
/** key: display port address */
@GuardedBy("mLock")
private final SparseArray<DisplayConfig> mDisplayPortConfigs = new SparseArray<>();
/** key: displayUniqueId */
@GuardedBy("mLock")
private final ArrayMap<String, DisplayConfig> mDisplayUniqueIdConfigs = new ArrayMap<>();
/** key: audio zone id */
@GuardedBy("mLock")
private final SparseIntArray mAudioZoneIdToOccupantZoneIdMapping = new SparseIntArray();
@VisibleForTesting
static class DisplayInfo {
public final Display display;
public final int displayType;
DisplayInfo(Display display, int displayType) {
this.display = display;
this.displayType = displayType;
}
@Override
public String toString() {
// do not include type as this is only used for dump
StringBuilder b = new StringBuilder(64);
b.append("{displayId=");
b.append(display.getDisplayId());
b.append(" displayType=");
b.append(displayType);
b.append("}");
return b.toString();
}
}
@VisibleForTesting
static class OccupantConfig {
public int userId = UserHandle.USER_NULL;
public final ArrayList<DisplayInfo> displayInfos = new ArrayList<>();
public int audioZoneId = CarAudioManager.INVALID_AUDIO_ZONE;
@Override
public String toString() {
// do not include type as this is only used for dump
StringBuilder b = new StringBuilder(128);
b.append("{userId=");
b.append(userId);
b.append(" displays=");
for (int i = 0; i < displayInfos.size(); i++) {
b.append(displayInfos.get(i).toString());
}
b.append(" audioZoneId=");
if (audioZoneId != CarAudioManager.INVALID_AUDIO_ZONE) {
b.append(audioZoneId);
} else {
b.append("none");
}
b.append("}");
return b.toString();
}
}
/** key : zoneId */
@GuardedBy("mLock")
private final SparseArray<OccupantConfig> mActiveOccupantConfigs = new SparseArray<>();
@GuardedBy("mLock")
private ICarServiceHelper mICarServiceHelper;
@GuardedBy("mLock")
private int mDriverZoneId = OccupantZoneInfo.INVALID_ZONE_ID;
@VisibleForTesting
final UserLifecycleListener mUserLifecycleListener = event -> {
Slogf.d(TAG, "onEvent(%s)", event);
if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) {
handleUserChange();
}
};
final CarUserService.PassengerCallback mPassengerCallback =
new CarUserService.PassengerCallback() {
@Override
public void onPassengerStarted(@UserIdInt int passengerId, int zoneId) {
handlePassengerStarted(passengerId, zoneId);
}
@Override
public void onPassengerStopped(@UserIdInt int passengerId) {
handlePassengerStopped(passengerId);
}
};
@VisibleForTesting
final DisplayManager.DisplayListener mDisplayListener =
new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
handleDisplayChange();
}
@Override
public void onDisplayRemoved(int displayId) {
handleDisplayChange();
}
@Override
public void onDisplayChanged(int displayId) {
// nothing to do
}
};
private final RemoteCallbackList<ICarOccupantZoneCallback> mClientCallbacks =
new RemoteCallbackList<>();
@GuardedBy("mLock")
private int mDriverSeat = VehicleAreaSeat.SEAT_UNKNOWN;
public CarOccupantZoneService(Context context) {
this(context, context.getSystemService(DisplayManager.class),
context.getSystemService(UserManager.class),
context.getResources().getBoolean(
R.bool.enableProfileUserAssignmentForMultiDisplay)
&& context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_MANAGED_USERS));
}
@VisibleForTesting
public CarOccupantZoneService(Context context, DisplayManager displayManager,
UserManager userManager, boolean enableProfileUserAssignmentForMultiDisplay) {
mContext = context;
mDisplayManager = displayManager;
mUserManager = userManager;
mEnableProfileUserAssignmentForMultiDisplay = enableProfileUserAssignmentForMultiDisplay;
}
@Override
public void init() {
// This does not require connection as binder will be passed directly.
Car car = new Car(mContext, /* service= */null, /* handler= */ null);
CarInfoManager infoManager = new CarInfoManager(car, CarLocalServices.getService(
CarPropertyService.class));
int driverSeat = infoManager.getDriverSeat();
synchronized (mLock) {
mDriverSeat = driverSeat;
parseOccupantZoneConfigsLocked();
parseDisplayConfigsLocked();
handleActiveDisplaysLocked();
handleAudioZoneChangesLocked();
handleUserChangesLocked();
}
CarUserService userService = CarLocalServices.getService(CarUserService.class);
userService.addUserLifecycleListener(mUserLifecycleListener);
userService.addPassengerCallback(mPassengerCallback);
mDisplayManager.registerDisplayListener(mDisplayListener,
new Handler(Looper.getMainLooper()));
CarUserService.ZoneUserBindingHelper helper = new CarUserService.ZoneUserBindingHelper() {
@Override
@NonNull
public List<OccupantZoneInfo> getOccupantZones(@OccupantTypeEnum int occupantType) {
List<OccupantZoneInfo> zones = new ArrayList<OccupantZoneInfo>();
for (OccupantZoneInfo ozi : getAllOccupantZones()) {
if (ozi.occupantType == occupantType) {
zones.add(ozi);
}
}
return zones;
}
@Override
public boolean assignUserToOccupantZone(@UserIdInt int userId, int zoneId) {
// Check if the user is already assigned to the other zone.
synchronized (mLock) {
for (int i = 0; i < mActiveOccupantConfigs.size(); ++i) {
OccupantConfig config = mActiveOccupantConfigs.valueAt(i);
if (config.userId == userId && zoneId != mActiveOccupantConfigs.keyAt(i)) {
Slogf.w(TAG, "cannot assign user to two different zone simultaneously");
return false;
}
}
OccupantConfig zoneConfig = mActiveOccupantConfigs.get(zoneId);
if (zoneConfig == null) {
Slogf.w(TAG, "cannot find the zone(%d)", zoneId);
return false;
}
if (zoneConfig.userId != UserHandle.USER_NULL && zoneConfig.userId != userId) {
Slogf.w(TAG, "other user already occupies the zone(%d)", zoneId);
return false;
}
zoneConfig.userId = userId;
return true;
}
}
@Override
public boolean unassignUserFromOccupantZone(@UserIdInt int userId) {
synchronized (mLock) {
for (int i = 0; i < mActiveOccupantConfigs.size(); ++i) {
OccupantConfig config = mActiveOccupantConfigs.valueAt(i);
if (config.userId == userId) {
config.userId = UserHandle.USER_NULL;
break;
}
}
return true;
}
}
@Override
public boolean isPassengerDisplayAvailable() {
for (OccupantZoneInfo ozi : getAllOccupantZones()) {
if (getDisplayForOccupant(ozi.zoneId,
CarOccupantZoneManager.DISPLAY_TYPE_MAIN) != Display.INVALID_DISPLAY
&& ozi.occupantType != CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) {
return true;
}
}
return false;
}
};
userService.setZoneUserBindingHelper(helper);
}
@Override
public void release() {
mDisplayManager.unregisterDisplayListener(mDisplayListener);
CarUserService userService = CarLocalServices.getService(CarUserService.class);
userService.removeUserLifecycleListener(mUserLifecycleListener);
userService.removePassengerCallback(mPassengerCallback);
synchronized (mLock) {
mOccupantsConfig.clear();
mDisplayPortConfigs.clear();
mDisplayUniqueIdConfigs.clear();
mAudioZoneIdToOccupantZoneIdMapping.clear();
mActiveOccupantConfigs.clear();
}
}
/** Return cloned mOccupantsConfig for testing */
@VisibleForTesting
@NonNull
public SparseArray<OccupantZoneInfo> getOccupantsConfig() {
synchronized (mLock) {
return mOccupantsConfig.clone();
}
}
/** Return cloned mDisplayPortConfigs for testing */
@VisibleForTesting
@NonNull
public SparseArray<DisplayConfig> getDisplayPortConfigs() {
synchronized (mLock) {
return mDisplayPortConfigs.clone();
}
}
/** Return cloned mDisplayUniqueIdConfigs for testing */
@VisibleForTesting
@NonNull
ArrayMap<String, DisplayConfig> getDisplayUniqueIdConfigs() {
synchronized (mLock) {
return new ArrayMap<>(mDisplayUniqueIdConfigs);
}
}
/** Return cloned mAudioConfigs for testing */
@VisibleForTesting
@NonNull
SparseIntArray getAudioConfigs() {
synchronized (mLock) {
return mAudioZoneIdToOccupantZoneIdMapping.clone();
}
}
/** Return cloned mActiveOccupantConfigs for testing */
@VisibleForTesting
@NonNull
public SparseArray<OccupantConfig> getActiveOccupantConfigs() {
synchronized (mLock) {
return mActiveOccupantConfigs.clone();
}
}
@Override
public void dump(IndentingPrintWriter writer) {
writer.println("*OccupantZoneService*");
synchronized (mLock) {
writer.println("**mOccupantsConfig**");
for (int i = 0; i < mOccupantsConfig.size(); ++i) {
writer.println(" zoneId=" + mOccupantsConfig.keyAt(i)
+ " info=" + mOccupantsConfig.valueAt(i));
}
writer.println("**mDisplayConfigs**");
for (int i = 0; i < mDisplayPortConfigs.size(); ++i) {
writer.println(" port=" + mDisplayPortConfigs.keyAt(i)
+ " config=" + mDisplayPortConfigs.valueAt(i));
}
for (int i = 0; i < mDisplayUniqueIdConfigs.size(); ++i) {
writer.println(" uniqueId=" + mDisplayUniqueIdConfigs.keyAt(i)
+ " config=" + mDisplayUniqueIdConfigs.valueAt(i));
}
writer.println("**mAudioZoneIdToOccupantZoneIdMapping**");
for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
writer.println(" audioZoneId=" + Integer.toHexString(audioZoneId)
+ " zoneId=" + mAudioZoneIdToOccupantZoneIdMapping.valueAt(index));
}
writer.println("**mActiveOccupantConfigs**");
for (int i = 0; i < mActiveOccupantConfigs.size(); ++i) {
writer.println(" zoneId=" + mActiveOccupantConfigs.keyAt(i)
+ " config=" + mActiveOccupantConfigs.valueAt(i));
}
writer.println("mEnableProfileUserAssignmentForMultiDisplay:"
+ mEnableProfileUserAssignmentForMultiDisplay);
writer.println("mEnableSourcePreferred:"
+ mEnableSourcePreferred);
writer.append("mSourcePreferredComponents: [");
if (mSourcePreferredComponents != null) {
for (int i = 0; i < mSourcePreferredComponents.size(); ++i) {
if (i > 0) writer.append(' ');
writer.append(mSourcePreferredComponents.get(i).toString());
}
}
writer.println(']');
}
}
@Override
public List<OccupantZoneInfo> getAllOccupantZones() {
synchronized (mLock) {
List<OccupantZoneInfo> infos = new ArrayList<>();
for (int i = 0; i < mActiveOccupantConfigs.size(); ++i) {
int zoneId = mActiveOccupantConfigs.keyAt(i);
// no need for deep copy as OccupantZoneInfo itself is static.
infos.add(mOccupantsConfig.get(zoneId));
}
return infos;
}
}
@Override
public int[] getAllDisplaysForOccupantZone(int occupantZoneId) {
synchronized (mLock) {
OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
if (config == null) {
return new int[0];
}
int[] displayIds = new int[config.displayInfos.size()];
for (int i = 0; i < config.displayInfos.size(); i++) {
displayIds[i] = config.displayInfos.get(i).display.getDisplayId();
}
return displayIds;
}
}
@Override
public int getDisplayForOccupant(int occupantZoneId, int displayType) {
synchronized (mLock) {
OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
if (config == null) {
return Display.INVALID_DISPLAY;
}
for (int i = 0; i < config.displayInfos.size(); i++) {
if (displayType == config.displayInfos.get(i).displayType) {
return config.displayInfos.get(i).display.getDisplayId();
}
}
}
return Display.INVALID_DISPLAY;
}
@Override
public int getDisplayIdForDriver(@DisplayTypeEnum int displayType) {
enforcePermission(Car.ACCESS_PRIVATE_DISPLAY_ID);
synchronized (mLock) {
int driverUserId = getDriverUserId();
DisplayInfo displayInfo = findDisplayForDriverLocked(driverUserId, displayType);
if (displayInfo == null) {
return Display.INVALID_DISPLAY;
}
return displayInfo.display.getDisplayId();
}
}
@Nullable
private DisplayInfo findDisplayForDriverLocked(int driverUserId,
@DisplayTypeEnum int displayType) {
for (OccupantZoneInfo zoneInfo : getAllOccupantZones()) {
if (zoneInfo.occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) {
OccupantConfig config = mActiveOccupantConfigs.get(zoneInfo.zoneId);
if (config == null) {
//No active display for zone, just continue...
continue;
}
if (config.userId == driverUserId) {
for (DisplayInfo displayInfo : config.displayInfos) {
if (displayInfo.displayType == displayType) {
return displayInfo;
}
}
}
}
}
return null;
}
@Override
public int getAudioZoneIdForOccupant(int occupantZoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
synchronized (mLock) {
OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
if (config != null) {
return config.audioZoneId;
}
// check if the occupant id exist at all
if (!mOccupantsConfig.contains(occupantZoneId)) {
return CarAudioManager.INVALID_AUDIO_ZONE;
}
// Exist but not active
return getAudioZoneIdForOccupantLocked(occupantZoneId);
}
}
private int getAudioZoneIdForOccupantLocked(int occupantZoneId) {
for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
if (occupantZoneId == mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId)) {
return audioZoneId;
}
}
return CarAudioManager.INVALID_AUDIO_ZONE;
}
@Override
public CarOccupantZoneManager.OccupantZoneInfo getOccupantForAudioZoneId(int audioZoneId) {
enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS);
synchronized (mLock) {
int occupantZoneId = mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId,
OccupantZoneInfo.INVALID_ZONE_ID);
if (occupantZoneId == OccupantZoneInfo.INVALID_ZONE_ID) {
return null;
}
// To support headless zones return the occupant configuration.
return mOccupantsConfig.get(occupantZoneId);
}
}
@Nullable
private DisplayConfig findDisplayConfigForDisplayIdLocked(int displayId) {
Display display = mDisplayManager.getDisplay(displayId);
if (display == null) {
return null;
}
return findDisplayConfigForDisplayLocked(display);
}
@Nullable
private DisplayConfig findDisplayConfigForDisplayLocked(Display display) {
int portAddress = getPortAddress(display);
if (portAddress != INVALID_PORT) {
DisplayConfig config = mDisplayPortConfigs.get(portAddress);
if (config != null) {
return config;
}
}
return mDisplayUniqueIdConfigs.get(display.getUniqueId());
}
@Override
public int getDisplayType(int displayId) {
synchronized (mLock) {
DisplayConfig config = findDisplayConfigForDisplayIdLocked(displayId);
if (config != null) {
return config.displayType;
}
}
return CarOccupantZoneManager.DISPLAY_TYPE_UNKNOWN;
}
@Override
public int getUserForOccupant(int occupantZoneId) {
synchronized (mLock) {
OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
if (config == null) {
return UserHandle.USER_NULL;
}
return config.userId;
}
}
@Override
public int getOccupantZoneIdForUserId(int userId) {
synchronized (mLock) {
for (int i = 0; i < mActiveOccupantConfigs.size(); ++i) {
OccupantConfig config = mActiveOccupantConfigs.valueAt(i);
if (config.userId == userId) {
return mActiveOccupantConfigs.keyAt(i);
}
}
Slogf.w(TAG, "Could not find occupantZoneId for userId%d returning invalid "
+ "occupant zone id %d", userId, OccupantZoneInfo.INVALID_ZONE_ID);
return OccupantZoneInfo.INVALID_ZONE_ID;
}
}
/**
* returns the current driver user id.
*/
public @UserIdInt int getDriverUserId() {
return getCurrentUser();
}
/**
* Sets the mapping for audio zone id to occupant zone id.
*
* @param audioZoneIdToOccupantZoneMapping map for audio zone id, where key is the audio zone id
* and value is the occupant zone id.
*/
public void setAudioZoneIdsForOccupantZoneIds(
@NonNull SparseIntArray audioZoneIdToOccupantZoneMapping) {
Objects.requireNonNull(audioZoneIdToOccupantZoneMapping,
"audioZoneIdToOccupantZoneMapping can not be null");
synchronized (mLock) {
validateOccupantZoneIdsLocked(audioZoneIdToOccupantZoneMapping);
mAudioZoneIdToOccupantZoneIdMapping.clear();
for (int index = 0; index < audioZoneIdToOccupantZoneMapping.size(); index++) {
int audioZoneId = audioZoneIdToOccupantZoneMapping.keyAt(index);
mAudioZoneIdToOccupantZoneIdMapping.put(audioZoneId,
audioZoneIdToOccupantZoneMapping.get(audioZoneId));
}
//If there are any active displays for the zone send change event
handleAudioZoneChangesLocked();
}
sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_AUDIO);
}
private void validateOccupantZoneIdsLocked(SparseIntArray audioZoneIdToOccupantZoneMapping) {
for (int i = 0; i < audioZoneIdToOccupantZoneMapping.size(); i++) {
int occupantZoneId =
audioZoneIdToOccupantZoneMapping.get(audioZoneIdToOccupantZoneMapping.keyAt(i));
if (!mOccupantsConfig.contains(occupantZoneId)) {
throw new IllegalArgumentException("occupantZoneId " + occupantZoneId
+ " does not exist.");
}
}
}
@Override
public void registerCallback(ICarOccupantZoneCallback callback) {
mClientCallbacks.register(callback);
}
@Override
public void unregisterCallback(ICarOccupantZoneCallback callback) {
mClientCallbacks.unregister(callback);
}
@Override
public boolean assignProfileUserToOccupantZone(int occupantZoneId, int userId) {
enforcePermission(android.Manifest.permission.MANAGE_USERS);
if (!mEnableProfileUserAssignmentForMultiDisplay) {
throw new IllegalStateException("feature not enabled");
}
int currentUser = getCurrentUser();
synchronized (mLock) {
if (occupantZoneId == mDriverZoneId) {
throw new IllegalArgumentException("Driver zone cannot have profile user");
}
updateEnabledProfilesLocked(currentUser);
if (!mProfileUsers.contains(userId) && userId != UserHandle.USER_NULL) {
// current user can change while this call is happening, so return false rather
// than throwing exception
Slogf.w(TAG, "Invalid profile user id: %d", userId);
return false;
}
if (!mUserManager.isUserRunning(userId)) {
Slogf.w(TAG, "User%d is not running.", userId);
return false;
}
OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId);
if (config == null) {
throw new IllegalArgumentException("Invalid occupantZoneId:" + occupantZoneId);
}
if (config.userId == userId && userId != UserHandle.USER_NULL) {
Slogf.w(TAG, "assignProfileUserToOccupantZone zone:%d already set to user:%",
occupantZoneId, userId);
return true;
}
if (userId == UserHandle.USER_NULL) {
config.userId = currentUser;
} else {
config.userId = userId;
}
}
sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
return true;
}
/**
* Sets {@code ICarServiceHelper}.
*/
public void setCarServiceHelper(ICarServiceHelper helper) {
doSyncWithCarServiceHelper(helper, /* updateDisplay= */ true, /* updateUser= */ true,
/* updateConfig= */ true);
}
private void doSyncWithCarServiceHelper(@Nullable ICarServiceHelper helper,
boolean updateDisplay, boolean updateUser, boolean updateConfig) {
int[] passengerDisplays = null;
ArrayMap<Integer, IntArray> allowlists = null;
ICarServiceHelper helperToUse = helper;
synchronized (mLock) {
if (helper == null) {
if (mICarServiceHelper == null) { // helper not set yet.
return;
}
helperToUse = mICarServiceHelper;
} else {
mICarServiceHelper = helper;
}
if (updateDisplay) {
passengerDisplays = getAllActivePassengerDisplaysLocked();
}
if (updateUser) {
allowlists = createDisplayAllowlistsLocked();
}
}
if (updateDisplay) {
updatePassengerDisplays(helperToUse, passengerDisplays);
}
if (updateUser) {
updateUserAssignmentForDisplays(helperToUse, allowlists);
}
if (updateConfig) {
Resources res = mContext.getResources();
String[] components = res.getStringArray(R.array.config_sourcePreferredComponents);
updateSourcePreferredComponents(helperToUse, components);
}
}
private int[] getAllActivePassengerDisplaysLocked() {
IntArray displays = new IntArray();
for (int j = 0; j < mActiveOccupantConfigs.size(); ++j) {
int zoneId = mActiveOccupantConfigs.keyAt(j);
if (zoneId == mDriverZoneId) {
continue;
}
OccupantConfig config = mActiveOccupantConfigs.valueAt(j);
for (int i = 0; i < config.displayInfos.size(); i++) {
displays.add(config.displayInfos.get(i).display.getDisplayId());
}
}
return displays.toArray();
}
private void updatePassengerDisplays(ICarServiceHelper helper, int[] passengerDisplayIds) {
if (passengerDisplayIds == null) {
return;
}
try {
helper.setPassengerDisplays(passengerDisplayIds);
} catch (RemoteException e) {
Slogf.e(TAG, "ICarServiceHelper.setPassengerDisplays failed", e);
}
}
private void updateSourcePreferredComponents(ICarServiceHelper helper, String[] components) {
boolean enableSourcePreferred;
ArrayList<ComponentName> componentNames = null;
if (components == null || components.length == 0) {
enableSourcePreferred = false;
Slogf.i(TAG, "CarLaunchParamsModifier: disable source-preferred");
} else if (components.length == 1 && components[0].equals(ALL_COMPONENTS)) {
enableSourcePreferred = true;
Slogf.i(TAG, "CarLaunchParamsModifier: enable source-preferred for all Components");
} else {
componentNames = new ArrayList<>((components.length));
for (String item : components) {
ComponentName name = ComponentName.unflattenFromString(item);
if (name == null) {
Slogf.e(TAG, "CarLaunchParamsModifier: Wrong ComponentName=" + item);
return;
}
componentNames.add(name);
}
enableSourcePreferred = true;
}
try {
helper.setSourcePreferredComponents(enableSourcePreferred, componentNames);
mEnableSourcePreferred = enableSourcePreferred;
mSourcePreferredComponents = componentNames;
} catch (RemoteException e) {
Slogf.e(TAG, "ICarServiceHelper.setSourcePreferredComponents failed");
}
}
private ArrayMap<Integer, IntArray> createDisplayAllowlistsLocked() {
ArrayMap<Integer, IntArray> allowlists = new ArrayMap<>();
for (int j = 0; j < mActiveOccupantConfigs.size(); ++j) {
int zoneId = mActiveOccupantConfigs.keyAt(j);
if (zoneId == mDriverZoneId) {
continue;
}
OccupantConfig config = mActiveOccupantConfigs.valueAt(j);
if (config.displayInfos.isEmpty()) {
continue;
}
// user like driver can have multiple zones assigned, so add them all.
IntArray displays = allowlists.get(config.userId);
if (displays == null) {
displays = new IntArray();
allowlists.put(config.userId, displays);
}
for (int i = 0; i < config.displayInfos.size(); i++) {
displays.add(config.displayInfos.get(i).display.getDisplayId());
}
}
return allowlists;
}
private void updateUserAssignmentForDisplays(ICarServiceHelper helper,
ArrayMap<Integer, IntArray> allowlists) {
if (allowlists == null || allowlists.isEmpty()) {
return;
}
try {
for (int i = 0; i < allowlists.size(); i++) {
int userId = allowlists.keyAt(i);
helper.setDisplayAllowlistForUser(userId, allowlists.valueAt(i).toArray());
}
} catch (RemoteException e) {
Slogf.e(TAG, "ICarServiceHelper.setDisplayAllowlistForUser failed", e);
}
}
private void throwFormatErrorInOccupantZones(String msg) {
throw new RuntimeException("Format error in config_occupant_zones resource:" + msg);
}
// For overriding in test
@VisibleForTesting
int getDriverSeat() {
synchronized (mLock) {
return mDriverSeat;
}
}
private void parseOccupantZoneConfigsLocked() {
final Resources res = mContext.getResources();
// examples:
// <item>occupantZoneId=0,occupantType=DRIVER,seatRow=1,seatSide=driver</item>
// <item>occupantZoneId=1,occupantType=FRONT_PASSENGER,seatRow=1,
// searSide=oppositeDriver</item>
boolean hasDriver = false;
int driverSeat = getDriverSeat();
int driverSeatSide = VehicleAreaSeat.SIDE_LEFT; // default LHD : Left Hand Drive
if (driverSeat == VehicleAreaSeat.SEAT_ROW_1_RIGHT) {
driverSeatSide = VehicleAreaSeat.SIDE_RIGHT;
}
int maxZoneId = OccupantZoneInfo.INVALID_ZONE_ID;
for (String config : res.getStringArray(R.array.config_occupant_zones)) {
int zoneId = OccupantZoneInfo.INVALID_ZONE_ID;
int type = CarOccupantZoneManager.OCCUPANT_TYPE_INVALID;
int seatRow = 0; // invalid row
int seatSide = VehicleAreaSeat.SIDE_LEFT;
String[] entries = config.split(",");
for (String entry : entries) {
String[] keyValuePair = entry.split("=");
if (keyValuePair.length != 2) {
throwFormatErrorInOccupantZones("No key/value pair:" + entry);
}
switch (keyValuePair[0]) {
case "occupantZoneId":
zoneId = Integer.parseInt(keyValuePair[1]);
break;
case "occupantType":
switch (keyValuePair[1]) {
case "DRIVER":
type = CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER;
break;
case "FRONT_PASSENGER":
type = CarOccupantZoneManager.OCCUPANT_TYPE_FRONT_PASSENGER;
break;
case "REAR_PASSENGER":
type = CarOccupantZoneManager.OCCUPANT_TYPE_REAR_PASSENGER;
break;
default:
throwFormatErrorInOccupantZones("Unrecognized type:" + entry);
break;
}
break;
case "seatRow":
seatRow = Integer.parseInt(keyValuePair[1]);
break;
case "seatSide":
switch (keyValuePair[1]) {
case "driver":
seatSide = driverSeatSide;
break;
case "oppositeDriver":
seatSide = -driverSeatSide;
break;
case "left":
seatSide = VehicleAreaSeat.SIDE_LEFT;
break;
case "center":
seatSide = VehicleAreaSeat.SIDE_CENTER;
break;
case "right":
seatSide = VehicleAreaSeat.SIDE_RIGHT;
break;
default:
throwFormatErrorInOccupantZones("Unregognized seatSide:" + entry);
break;
}
break;
default:
throwFormatErrorInOccupantZones("Unrecognized key:" + entry);
break;
}
}
if (zoneId == OccupantZoneInfo.INVALID_ZONE_ID) {
throwFormatErrorInOccupantZones("Missing zone id:" + config);
}
if (zoneId > maxZoneId) {
maxZoneId = zoneId;
}
if (type == CarOccupantZoneManager.OCCUPANT_TYPE_INVALID) {
throwFormatErrorInOccupantZones("Missing type:" + config);
}
if (type == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) {
if (hasDriver) {
throwFormatErrorInOccupantZones("Multiple driver:" + config);
} else {
hasDriver = true;
mDriverZoneId = zoneId;
}
}
int seat = VehicleAreaSeat.fromRowAndSide(seatRow, seatSide);
if (seat == VehicleAreaSeat.SEAT_UNKNOWN) {
throwFormatErrorInOccupantZones("Invalid seat:" + config);
}
OccupantZoneInfo info = new OccupantZoneInfo(zoneId, type, seat);
if (mOccupantsConfig.contains(zoneId)) {
throwFormatErrorInOccupantZones("Duplicate zone id:" + config);
}
mOccupantsConfig.put(zoneId, info);
}
if (!hasDriver) {
maxZoneId++;
mDriverZoneId = maxZoneId;
Slogf.w(TAG, "No driver zone, add one:%d", mDriverZoneId);
OccupantZoneInfo info = new OccupantZoneInfo(mDriverZoneId,
CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER, getDriverSeat());
mOccupantsConfig.put(mDriverZoneId, info);
}
}
private void throwFormatErrorInDisplayMapping(String msg) {
throw new RuntimeException(
"Format error in config_occupant_display_mapping resource:" + msg);
}
private void parseDisplayConfigsLocked() {
final Resources res = mContext.getResources();
// examples:
// <item>displayPort=0,displayType=MAIN,occupantZoneId=0</item>
// <item>displayPort=1,displayType=INSTRUMENT_CLUSTER,occupantZoneId=0</item>
for (String config : res.getStringArray(R.array.config_occupant_display_mapping)) {
int port = INVALID_PORT;
String uniqueId = null;
int type = CarOccupantZoneManager.DISPLAY_TYPE_UNKNOWN;
int zoneId = OccupantZoneInfo.INVALID_ZONE_ID;
String[] entries = config.split(",");
for (String entry : entries) {
String[] keyValuePair = entry.split("=");
if (keyValuePair.length != 2) {
throwFormatErrorInDisplayMapping("No key/value pair:" + entry);
}
switch (keyValuePair[0]) {
case "displayPort":
port = Integer.parseInt(keyValuePair[1]);
break;
case "displayUniqueId":
uniqueId = keyValuePair[1];
break;
case "displayType":
switch (keyValuePair[1]) {
case "MAIN":
type = CarOccupantZoneManager.DISPLAY_TYPE_MAIN;
break;
case "INSTRUMENT_CLUSTER":
type = CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER;
break;
case "HUD":
type = CarOccupantZoneManager.DISPLAY_TYPE_HUD;
break;
case "INPUT":
type = CarOccupantZoneManager.DISPLAY_TYPE_INPUT;
break;
case "AUXILIARY":
type = CarOccupantZoneManager.DISPLAY_TYPE_AUXILIARY;
break;
default:
throwFormatErrorInDisplayMapping(
"Unrecognized display type:" + entry);
break;
}
break;
case "occupantZoneId":
zoneId = Integer.parseInt(keyValuePair[1]);
break;
default:
throwFormatErrorInDisplayMapping("Unrecognized key:" + entry);
break;
}
}
// Now check validity
if (port == INVALID_PORT && uniqueId == null) {
throwFormatErrorInDisplayMapping(
"Missing or invalid displayPort and displayUniqueId:" + config);
}
if (type == CarOccupantZoneManager.DISPLAY_TYPE_UNKNOWN) {
throwFormatErrorInDisplayMapping("Missing or invalid displayType:" + config);
}
if (zoneId == OccupantZoneInfo.INVALID_ZONE_ID) {
throwFormatErrorInDisplayMapping("Missing or invalid occupantZoneId:" + config);
}
if (!mOccupantsConfig.contains(zoneId)) {
throwFormatErrorInDisplayMapping(
"Missing or invalid occupantZoneId:" + config);
}
DisplayConfig displayConfig = new DisplayConfig(type, zoneId);
if (port != INVALID_PORT) {
if (mDisplayPortConfigs.contains(port)) {
throwFormatErrorInDisplayMapping("Duplicate displayPort:" + config);
}
mDisplayPortConfigs.put(port, displayConfig);
} else {
if (mDisplayUniqueIdConfigs.containsKey(uniqueId)) {
throwFormatErrorInDisplayMapping("Duplicate displayUniqueId:" + config);
}
mDisplayUniqueIdConfigs.put(uniqueId, displayConfig);
}
}
}
private int getPortAddress(Display display) {
DisplayAddress address = display.getAddress();
if (address instanceof DisplayAddress.Physical) {
DisplayAddress.Physical physicalAddress = (DisplayAddress.Physical) address;
if (physicalAddress != null) {
return physicalAddress.getPort();
}
}
return INVALID_PORT;
}
private void addDisplayInfoToOccupantZoneLocked(int zoneId, DisplayInfo info) {
OccupantConfig occupantConfig = mActiveOccupantConfigs.get(zoneId);
if (occupantConfig == null) {
occupantConfig = new OccupantConfig();
mActiveOccupantConfigs.put(zoneId, occupantConfig);
}
occupantConfig.displayInfos.add(info);
}
private void handleActiveDisplaysLocked() {
mActiveOccupantConfigs.clear();
boolean hasDefaultDisplayConfig = false;
for (Display display : mDisplayManager.getDisplays()) {
DisplayConfig displayConfig = findDisplayConfigForDisplayLocked(display);
if (displayConfig == null) {
Slogf.w(TAG, "Display id:%d does not have configurations",
display.getDisplayId());
continue;
}
if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
if (displayConfig.occupantZoneId != mDriverZoneId) {
throw new IllegalStateException(
"Default display should be only assigned to driver zone");
}
hasDefaultDisplayConfig = true;
}
addDisplayInfoToOccupantZoneLocked(displayConfig.occupantZoneId,
new DisplayInfo(display, displayConfig.displayType));
}
if (!hasDefaultDisplayConfig) {
// Can reach here if default display has no port / no config
Slogf.w(TAG, "Default display not assigned, will assign to driver zone");
addDisplayInfoToOccupantZoneLocked(mDriverZoneId, new DisplayInfo(
mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY),
CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
}
}
@VisibleForTesting
int getCurrentUser() {
return ActivityManager.getCurrentUser();
}
private void updateEnabledProfilesLocked(int userId) {
mProfileUsers.clear();
List<UserInfo> profileUsers = mUserManager.getEnabledProfiles(userId);
for (UserInfo userInfo : profileUsers) {
if (userInfo.id != userId) {
mProfileUsers.add(userInfo.id);
}
}
}
private void handleUserChangesLocked() {
int driverUserId = getCurrentUser();
if (mEnableProfileUserAssignmentForMultiDisplay) {
updateEnabledProfilesLocked(driverUserId);
}
for (int i = 0; i < mActiveOccupantConfigs.size(); ++i) {
int zoneId = mActiveOccupantConfigs.keyAt(i);
OccupantConfig config = mActiveOccupantConfigs.valueAt(i);
// mProfileUsers empty if not supported
if (mProfileUsers.contains(config.userId)) {
Slogf.i(TAG, "Profile user:%d already assigned for occupant zone:%d",
config.userId, zoneId);
} else {
config.userId = driverUserId;
}
}
}
private void handleAudioZoneChangesLocked() {
for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
int occupantZoneId = mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId);
OccupantConfig occupantConfig = mActiveOccupantConfigs.get(occupantZoneId);
if (occupantConfig == null) {
//no active display for zone just continue
continue;
}
// Found an active configuration, add audio to it.
occupantConfig.audioZoneId = audioZoneId;
}
}
private void sendConfigChangeEvent(int changeFlags) {
boolean updateDisplay = false;
boolean updateUser = false;
if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) != 0) {
updateDisplay = true;
updateUser = true;
} else if ((changeFlags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER) != 0) {
updateUser = true;
}
doSyncWithCarServiceHelper(/* helper= */ null, updateDisplay, updateUser,
/* updateConfig= */ false);
final int n = mClientCallbacks.beginBroadcast();
for (int i = 0; i < n; i++) {
ICarOccupantZoneCallback callback = mClientCallbacks.getBroadcastItem(i);
try {
callback.onOccupantZoneConfigChanged(changeFlags);
} catch (RemoteException ignores) {
// ignore
}
}
mClientCallbacks.finishBroadcast();
}
private void handleUserChange() {
synchronized (mLock) {
handleUserChangesLocked();
}
sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
}
private void handlePassengerStarted(@UserIdInt int passengerId, int zoneId) {
sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
}
private void handlePassengerStopped(@UserIdInt int passengerId) {
sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER);
}
private void handleDisplayChange() {
synchronized (mLock) {
handleActiveDisplaysLocked();
//audio zones should be re-checked for changed display
handleAudioZoneChangesLocked();
// user should be re-checked for changed displays
handleUserChangesLocked();
}
sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY);
}
private void enforcePermission(String permissionName) {
if (mContext.checkCallingOrSelfPermission(permissionName)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("requires permission " + permissionName);
}
}
}