/* * 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 static android.car.builtin.view.DisplayHelper.INVALID_PORT; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPING; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING; import static android.view.Display.STATE_ON; import static com.android.car.CarServiceUtils.getHandlerThread; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import static com.android.car.internal.common.CommonConstants.EMPTY_INT_ARRAY; 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.builtin.util.Slogf; import android.car.builtin.view.DisplayHelper; import android.car.input.CarInputManager; import android.car.media.CarAudioManager; import android.car.user.CarUserManager.UserLifecycleListener; import android.car.user.UserLifecycleEventFilter; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; import android.hardware.display.DisplayManager; import android.os.Binder; 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.Log; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; import android.view.Display; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; import com.android.car.internal.util.IntArray; import com.android.car.occupantzone.CarOccupantZoneDumpProto; import com.android.car.occupantzone.CarOccupantZoneDumpProto.DisplayConfigProto; import com.android.car.occupantzone.CarOccupantZoneDumpProto.DisplayPortConfigsProto; import com.android.car.occupantzone.CarOccupantZoneDumpProto.DisplayPortConfigsProto.DisplayConfigPortProto; import com.android.car.occupantzone.CarOccupantZoneDumpProto.DisplayUniqueIdConfigsProto; import com.android.car.occupantzone.CarOccupantZoneDumpProto.DisplayUniqueIdConfigsProto.DisplayConfigUniqueIdProto; import com.android.car.user.CarUserService; import com.android.car.user.ExperimentalCarUserService; import com.android.car.user.ExperimentalCarUserService.ZoneUserBindingHelper; import com.android.car.user.UserHandleHelper; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Arrays; 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 boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); private static final String HANDLER_THREAD_NAME = "CarOccupantZoneService"; private static final int[] EMPTY_INPUT_SUPPORT_TYPES = EMPTY_INT_ARRAY; private final Object mLock = new Object(); private final Context mContext; private final DisplayManager mDisplayManager; private final UserManager mUserManager; private CarUserService mCarUserService; private final boolean mEnableProfileUserAssignmentForMultiDisplay; /** * Stores android user id of profile users for the current user. */ @GuardedBy("mLock") private final ArraySet mProfileUsers = new ArraySet<>(); /** key: zone id */ @GuardedBy("mLock") private final SparseArray mOccupantsConfig = new SparseArray<>(); /** * The config of a display identified by occupant zone id and display type. */ public static final class DisplayConfig { public final int displayType; public final int occupantZoneId; public final int[] inputTypes; DisplayConfig(int displayType, int occupantZoneId, IntArray inputTypes) { this.displayType = displayType; this.occupantZoneId = occupantZoneId; if (inputTypes == null) { Slogf.w(TAG, "No input type was defined for displayType:%d " + " and occupantZoneId:%d", displayType, occupantZoneId); } this.inputTypes = inputTypes == null ? EMPTY_INPUT_SUPPORT_TYPES : inputTypes.toArray(); } @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(" inputTypes="); b.append(Arrays.toString(inputTypes)); b.append("}"); return b.toString(); } } /** key: display port address */ @GuardedBy("mLock") private final SparseArray mDisplayPortConfigs = new SparseArray<>(); /** key: displayUniqueId */ @GuardedBy("mLock") private final ArrayMap 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 = CarOccupantZoneManager.INVALID_USER_ID; public final ArrayList 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 mActiveOccupantConfigs = new SparseArray<>(); @GuardedBy("mLock") private int mDriverZoneId = OccupantZoneInfo.INVALID_ZONE_ID; @VisibleForTesting final UserLifecycleListener mUserLifecycleListener = event -> { if (DBG) Slogf.d(TAG, "onEvent(%s)", event); handleUserChange(); }; final ExperimentalCarUserService.PassengerCallback mPassengerCallback = new ExperimentalCarUserService.PassengerCallback() { @Override public void onPassengerStarted(@UserIdInt int passengerId, int zoneId) { handlePassengerStarted(); } @Override public void onPassengerStopped(@UserIdInt int passengerId) { handlePassengerStopped(); } }; @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 mClientCallbacks = new RemoteCallbackList<>(); @GuardedBy("mLock") private int mDriverSeat = VehicleAreaSeat.SEAT_UNKNOWN; private final UserHandleHelper mUserHandleHelper; final Handler mHandler = new Handler(getHandlerThread(HANDLER_THREAD_NAME).getLooper()); 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), new UserHandleHelper(context, context.getSystemService(UserManager.class))); } @VisibleForTesting public CarOccupantZoneService(Context context, DisplayManager displayManager, UserManager userManager, boolean enableProfileUserAssignmentForMultiDisplay, UserHandleHelper userHandleHelper) { mContext = context; mDisplayManager = displayManager; mUserManager = userManager; mEnableProfileUserAssignmentForMultiDisplay = enableProfileUserAssignmentForMultiDisplay; mUserHandleHelper = userHandleHelper; } @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(); } mCarUserService = CarLocalServices.getService(CarUserService.class); UserLifecycleEventFilter userEventFilter = new UserLifecycleEventFilter.Builder() .addEventType(USER_LIFECYCLE_EVENT_TYPE_SWITCHING).addEventType( USER_LIFECYCLE_EVENT_TYPE_STOPPING).build(); mCarUserService.addUserLifecycleListener(userEventFilter, mUserLifecycleListener); ExperimentalCarUserService experimentalUserService = CarLocalServices.getService(ExperimentalCarUserService.class); if (experimentalUserService != null) { experimentalUserService.addPassengerCallback(mPassengerCallback); } mDisplayManager.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper())); ZoneUserBindingHelper helper = new ZoneUserBindingHelper() { @Override @NonNull public List getOccupantZones(@OccupantTypeEnum int occupantType) { List zones = new ArrayList(); 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) { int userZoneId = getZoneIdForUserIdLocked(userId); if (userZoneId != OccupantZoneInfo.INVALID_ZONE_ID && mActiveOccupantConfigs.keyAt(userZoneId) != zoneId) { Slogf.w(TAG, "Cannot assign user to two different zones 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 != CarOccupantZoneManager.INVALID_USER_ID && 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 = CarOccupantZoneManager.INVALID_USER_ID; 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; } }; if (experimentalUserService != null) { experimentalUserService.setZoneUserBindingHelper(helper); } CarServiceHelperWrapper.getInstance().runOnConnection(() -> doSyncWithCarServiceHelper( /* updateDisplay= */ true, /* updateUser= */ true)); } @Override public void release() { mDisplayManager.unregisterDisplayListener(mDisplayListener); mCarUserService.removeUserLifecycleListener(mUserLifecycleListener); ExperimentalCarUserService experimentalUserService = CarLocalServices.getService(ExperimentalCarUserService.class); if (experimentalUserService != null) { experimentalUserService.removePassengerCallback(mPassengerCallback); } synchronized (mLock) { mOccupantsConfig.clear(); mDisplayPortConfigs.clear(); mDisplayUniqueIdConfigs.clear(); mAudioZoneIdToOccupantZoneIdMapping.clear(); mActiveOccupantConfigs.clear(); } } /** Return cloned mOccupantsConfig for testing */ @VisibleForTesting @NonNull public SparseArray getOccupantsConfig() { synchronized (mLock) { return mOccupantsConfig.clone(); } } /** Return cloned mDisplayPortConfigs for testing */ @VisibleForTesting @NonNull public SparseArray getDisplayPortConfigs() { synchronized (mLock) { return mDisplayPortConfigs.clone(); } } /** Return cloned mDisplayUniqueIdConfigs for testing */ @VisibleForTesting @NonNull ArrayMap 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 getActiveOccupantConfigs() { synchronized (mLock) { return mActiveOccupantConfigs.clone(); } } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) 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("hasDriverZone: " + hasDriverZone()); } } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) { synchronized (mLock) { for (int i = 0; i < mDisplayPortConfigs.size(); i++) { long displayPortConfigsToken = proto.start( CarOccupantZoneDumpProto.DISPLAY_PORT_CONFIGS); long displayConfigPortToken = proto.start( DisplayPortConfigsProto.DISPLAY_CONFIG_PORT); int port = mDisplayPortConfigs.keyAt(i); proto.write(DisplayConfigPortProto.PORT, port); long displayConfigToken = proto.start(DisplayConfigPortProto.DISPLAY_CONFIG); DisplayConfig displayConfig = mDisplayPortConfigs.valueAt(i); proto.write(DisplayConfigProto.DISPLAY_TYPE, displayConfig.displayType); proto.write(DisplayConfigProto.OCCUPANT_ZONE_ID, displayConfig.occupantZoneId); for (int j = 0; j < displayConfig.inputTypes.length; j++) { proto.write(DisplayConfigProto.INPUT_TYPES, displayConfig.inputTypes[j]); } proto.end(displayConfigToken); proto.end(displayConfigPortToken); proto.end(displayPortConfigsToken); } for (int i = 0; i < mDisplayUniqueIdConfigs.size(); i++) { long displayUniqueIdConfigsToken = proto.start( CarOccupantZoneDumpProto.DISPLAY_UNIQUE_ID_CONFIGS); long displayConfigUniqueIdToken = proto.start( DisplayUniqueIdConfigsProto.DISPLAY_CONFIG_UNIQUE_ID); String uniqueId = mDisplayUniqueIdConfigs.keyAt(i); proto.write(DisplayConfigUniqueIdProto.UNIQUE_ID, uniqueId); long displayConfigToken = proto.start(DisplayConfigPortProto.DISPLAY_CONFIG); DisplayConfig displayConfig = mDisplayUniqueIdConfigs.valueAt(i); proto.write(DisplayConfigProto.DISPLAY_TYPE, displayConfig.displayType); proto.write(DisplayConfigProto.OCCUPANT_ZONE_ID, displayConfig.occupantZoneId); for (int j = 0; j < displayConfig.inputTypes.length; j++) { proto.write(DisplayConfigProto.INPUT_TYPES, displayConfig.inputTypes[j]); } proto.end(displayConfigToken); proto.end(displayConfigUniqueIdToken); proto.end(displayUniqueIdConfigsToken); } } } @Override public List getAllOccupantZones() { synchronized (mLock) { List 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 EMPTY_INT_ARRAY; } 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; } } /** * Checks if all displays for a given OccupantZone are on. * * @param occupantZoneId indicates which OccupantZone's displays to check * @return whether all displays for a given OccupantZone are on */ public boolean areDisplaysOnForOccupantZone(int occupantZoneId) { synchronized (mLock) { OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId); if (config == null) { return false; } for (int i = 0; i < config.displayInfos.size(); i++) { if (config.displayInfos.get(i).display.getState() != STATE_ON) { return false; } } return true; } } @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; } public IntArray getAllDisplayIdsForDriver(int displayType) { synchronized (mLock) { OccupantConfig config = mActiveOccupantConfigs.get(mDriverZoneId); if (config == null) { return new IntArray(0); } IntArray displayIds = new IntArray(config.displayInfos.size()); for (int i = 0; i < config.displayInfos.size(); i++) { DisplayInfo displayInfo = config.displayInfos.get(i); if (displayInfo.displayType == displayType) { displayIds.add(displayInfo.display.getDisplayId()); } } return displayIds; } } @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(); } } @GuardedBy("mLock") @Nullable private DisplayInfo findDisplayForDriverLocked(@UserIdInt 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); } } @GuardedBy("mLock") 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 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); } } /** * Finds the DisplayConfig for a logical display id. */ @Nullable public DisplayConfig findDisplayConfigForDisplayId(int displayId) { synchronized (mLock) { return findDisplayConfigForDisplayIdLocked(displayId); } } /** * Finds the DisplayConfig for a physical display port. */ @Nullable public DisplayConfig findDisplayConfigForPort(int portAddress) { synchronized (mLock) { return findDisplayConfigForPortLocked(portAddress); } } @GuardedBy("mLock") @Nullable private DisplayConfig findDisplayConfigForDisplayIdLocked(int displayId) { Display display = mDisplayManager.getDisplay(displayId); if (display == null) { return null; } return findDisplayConfigForDisplayLocked(display); } @GuardedBy("mLock") @Nullable private DisplayConfig findDisplayConfigForDisplayLocked(Display display) { int portAddress = DisplayHelper.getPhysicalPort(display); if (portAddress != INVALID_PORT) { DisplayConfig config = mDisplayPortConfigs.get(portAddress); if (config != null) { return config; } } return mDisplayUniqueIdConfigs.get(DisplayHelper.getUniqueId(display)); } @GuardedBy("mLock") @Nullable private DisplayConfig findDisplayConfigForPortLocked(int portAddress) { return portAddress != INVALID_PORT ? mDisplayPortConfigs.get(portAddress) : null; } @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 CarOccupantZoneManager.INVALID_USER_ID; } return config.userId; } } @Override public int getOccupantZoneIdForUserId(@UserIdInt 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; } } @Override public OccupantZoneInfo getOccupantZoneForDisplayId(int displayId) { synchronized (mLock) { DisplayConfig displayConfig = findDisplayConfigForDisplayIdLocked(displayId); if (displayConfig == null) { Slogf.w(TAG, "getOccupantZoneForDisplayId: Could not find DisplayConfig for " + "display Id %d", displayId); return null; } int occupantZoneId = displayConfig.occupantZoneId; if (occupantZoneId == OccupantZoneInfo.INVALID_ZONE_ID) { Slogf.w(TAG, "getOccupantZoneForDisplayId: Got invalid occupant zone id from " + "DisplayConfig: %s", displayConfig); return null; } return mOccupantsConfig.get(occupantZoneId); } } /** * 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); } @GuardedBy("mLock") 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, @UserIdInt int userId) { CarServiceUtils.assertAnyPermission(mContext, android.Manifest.permission.MANAGE_USERS, Car.PERMISSION_MANAGE_OCCUPANT_ZONE); if (!mEnableProfileUserAssignmentForMultiDisplay) { throw new IllegalStateException("feature not enabled"); } UserHandle user = null; synchronized (mLock) { if (occupantZoneId == mDriverZoneId) { throw new IllegalArgumentException("Driver zone cannot have profile user"); } updateEnabledProfilesLocked(getCurrentUser()); if (!mProfileUsers.contains(userId) && userId != CarOccupantZoneManager.INVALID_USER_ID) { // 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 (userId != CarOccupantZoneManager.INVALID_USER_ID) { user = UserHandle.of(userId); } } long token = Binder.clearCallingIdentity(); try { if (userId == CarOccupantZoneManager.INVALID_USER_ID) { return unassignOccupantZoneUnchecked(occupantZoneId) == CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_OK; } else { return assignVisibleUserToOccupantZoneUnchecked(occupantZoneId, user) == CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_OK; } } finally { Binder.restoreCallingIdentity(token); } } @Override public int assignVisibleUserToOccupantZone(int occupantZoneId, UserHandle user) { CarServiceUtils.assertAnyPermission(mContext, android.Manifest.permission.MANAGE_USERS, Car.PERMISSION_MANAGE_OCCUPANT_ZONE); Preconditions.checkNotNull(user); long token = Binder.clearCallingIdentity(); try { return assignVisibleUserToOccupantZoneUnchecked(occupantZoneId, user); } finally { Binder.restoreCallingIdentity(token); } } /** * Precondition: permission check should be done and binder caller identity should be cleared. */ private int assignVisibleUserToOccupantZoneUnchecked(int occupantZoneId, @NonNull UserHandle user) { int userId; if (user.equals(UserHandle.CURRENT)) { userId = getCurrentUser(); } else { userId = user.getIdentifier(); } if (!mCarUserService.isUserVisible(userId)) { Slogf.w(TAG, "Non-visible user %d cannot be allocated to zone %d", userId, occupantZoneId); return CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_FAIL_NON_VISIBLE_USER; } synchronized (mLock) { int userZoneId = getZoneIdForUserIdLocked(userId); if (userZoneId != OccupantZoneInfo.INVALID_ZONE_ID && mActiveOccupantConfigs.keyAt(userZoneId) != occupantZoneId) { Slogf.w(TAG, "Cannot assign visible user %d to two different zones simultaneously," + " user is already assigned to %d", userId, userZoneId); return CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_FAIL_ALREADY_ASSIGNED; } OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId); if (config == null) { Slogf.w(TAG, "Invalid zone:%d", occupantZoneId); throw new IllegalArgumentException("Invalid occupantZoneId:" + occupantZoneId); } if (config.userId == userId && userId != CarOccupantZoneManager.INVALID_USER_ID) { Slogf.w(TAG, "assignVisibleUserToOccupantZone zone:%d already set to user:%d", occupantZoneId, userId); return CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_OK; } if (DBG) Slogf.d(TAG, "Assigned user %d to zone %d", userId, occupantZoneId); config.userId = userId; } sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER); return CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_OK; } @GuardedBy("mLock") private int getZoneIdForUserIdLocked(@UserIdInt int userId) { for (int i = 0; i < mActiveOccupantConfigs.size(); i++) { OccupantConfig config = mActiveOccupantConfigs.valueAt(i); if (config.userId == userId) { return mActiveOccupantConfigs.keyAt(i); } } return OccupantZoneInfo.INVALID_ZONE_ID; } @Override public int unassignOccupantZone(int occupantZoneId) { CarServiceUtils.assertAnyPermission(mContext, android.Manifest.permission.MANAGE_USERS, Car.PERMISSION_MANAGE_OCCUPANT_ZONE); long token = Binder.clearCallingIdentity(); try { return unassignOccupantZoneUnchecked(occupantZoneId); } finally { Binder.restoreCallingIdentity(token); } } /** * Precondition: permission check should be done and binder caller identity should be cleared. */ private int unassignOccupantZoneUnchecked(int occupantZoneId) { synchronized (mLock) { OccupantConfig config = mActiveOccupantConfigs.get(occupantZoneId); if (config == null) { Slogf.w(TAG, "Invalid zone:%d", occupantZoneId); throw new IllegalArgumentException("Invalid occupantZoneId:" + occupantZoneId); } if (config.userId == CarOccupantZoneManager.INVALID_USER_ID) { // already unassigned Slogf.w(TAG, "unassignOccupantZone for already unassigned zone:%d", occupantZoneId); return CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_OK; } OccupantZoneInfo info = mOccupantsConfig.get(occupantZoneId); if (info.occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER) { Slogf.w(TAG, "Cannot unassign driver zone"); return CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_FAIL_DRIVER_ZONE; } if (DBG) Slogf.d(TAG, "Unassigned zone:%d", occupantZoneId); config.userId = CarOccupantZoneManager.INVALID_USER_ID; } sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER); return CarOccupantZoneManager.USER_ASSIGNMENT_RESULT_OK; } @Override public OccupantZoneInfo getMyOccupantZone() { int uid = Binder.getCallingUid(); // UserHandle.getUserId(uid) can do this in one step but it is hidden API. UserHandle user = UserHandle.getUserHandleForUid(uid); int userId = user.getIdentifier(); synchronized (mLock) { for (int i = 0; i < mActiveOccupantConfigs.size(); i++) { OccupantConfig config = mActiveOccupantConfigs.valueAt(i); if (config.userId == userId) { int zoneId = mActiveOccupantConfigs.keyAt(i); return mOccupantsConfig.get(zoneId); } } } Slogf.w(TAG, "getMyOccupantZone: No assigned zone for uid:%d", uid); return null; } @Override public OccupantZoneInfo getOccupantZoneForUser(UserHandle user) { Objects.requireNonNull(user, "User cannot be null"); if (user.getIdentifier() == CarOccupantZoneManager.INVALID_USER_ID) { return null; } int occupantZoneId = getOccupantZoneIdForUserId(user.getIdentifier()); if (DBG) Slogf.d(TAG, "occupantZoneId that was gotten was %d", occupantZoneId); synchronized (mLock) { return mOccupantsConfig.get(occupantZoneId); } } @Override public OccupantZoneInfo getOccupantZone(@OccupantTypeEnum int occupantType, @VehicleAreaSeat.Enum int seat) { synchronized (mLock) { for (int i = 0; i < mActiveOccupantConfigs.size(); i++) { int zoneId = mActiveOccupantConfigs.keyAt(i); OccupantZoneInfo info = mOccupantsConfig.get(zoneId); if (occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER || occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_FRONT_PASSENGER) { if (occupantType == info.occupantType) { return info; } } else { if (occupantType == info.occupantType && seat == info.seat) { return info; } } } return null; } } /** * Gets the occupant zone id for the seat * * @param seat The vehicle area seat to be used * * @return The occupant zone id for the given seat */ public int getOccupantZoneIdForSeat(@VehicleAreaSeat.Enum int seat) { synchronized (mLock) { return getOccupantZoneIdForSeatLocked(seat); } } @GuardedBy("mLock") private int getOccupantZoneIdForSeatLocked(@VehicleAreaSeat.Enum int seat) { for (int i = 0; i < mActiveOccupantConfigs.size(); i++) { int zoneId = mActiveOccupantConfigs.keyAt(i); OccupantZoneInfo info = mOccupantsConfig.get(zoneId); if (seat == info.seat) { return zoneId; } } return OccupantZoneInfo.INVALID_ZONE_ID; } @Override public boolean hasDriverZone() { synchronized (mLock) { return mDriverZoneId != OccupantZoneInfo.INVALID_ZONE_ID; } } @Override public boolean hasPassengerZones() { synchronized (mLock) { // There can be only one driver zone. So if there is driver, there should be at least // two zones to have passenger. If there is no driver zone, having a zone is enough to // have passenger zone. boolean hasDriver = mDriverZoneId != OccupantZoneInfo.INVALID_ZONE_ID; return mActiveOccupantConfigs.size() > (hasDriver ? 1 : 0); } } @Override @UserIdInt public int getUserForDisplayId(int displayId) { synchronized (mLock) { for (int i = 0; i < mActiveOccupantConfigs.size(); i++) { OccupantConfig config = mActiveOccupantConfigs.valueAt(i); for (int j = 0; j < config.displayInfos.size(); j++) { if (config.displayInfos.get(j).display.getDisplayId() == displayId) { return config.userId; } } } } Slogf.w(TAG, "Could not find OccupantZone for display Id %d", displayId); return CarOccupantZoneManager.INVALID_USER_ID; } /** Returns number of passenger zones in the device. */ public int getNumberOfPassengerZones() { synchronized (mLock) { boolean hasDriver = mDriverZoneId != OccupantZoneInfo.INVALID_ZONE_ID; return mActiveOccupantConfigs.size() - (hasDriver ? 1 : 0); } } private void doSyncWithCarServiceHelper(boolean updateDisplay, boolean updateUser) { int[] passengerDisplays = null; ArrayMap allowlists = null; synchronized (mLock) { if (updateDisplay) { passengerDisplays = getAllActivePassengerDisplaysLocked(); } if (updateUser) { allowlists = createDisplayAllowlistsLocked(); } } if (updateDisplay) { updatePassengerDisplays(passengerDisplays); } if (updateUser) { updateUserAssignmentForDisplays(allowlists); } } @GuardedBy("mLock") 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(int[] passengerDisplayIds) { if (passengerDisplayIds == null) { return; } CarServiceHelperWrapper.getInstance().setPassengerDisplays(passengerDisplayIds); } @GuardedBy("mLock") private ArrayMap createDisplayAllowlistsLocked() { ArrayMap 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; } // Do not allow any user if it is unassigned. if (config.userId == CarOccupantZoneManager.INVALID_USER_ID) { continue; } 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(ArrayMap allowlists) { if (allowlists == null || allowlists.isEmpty()) { return; } for (int i = 0; i < allowlists.size(); i++) { int userId = allowlists.keyAt(i); CarServiceHelperWrapper.getInstance().setDisplayAllowlistForUser(userId, allowlists.valueAt(i).toArray()); } } private void throwFormatErrorInOccupantZones(String msg) { throw new RuntimeException("Format error in config_occupant_zones resource:" + msg); } /** Returns the driver seat. */ int getDriverSeat() { synchronized (mLock) { return mDriverSeat; } } @GuardedBy("mLock") private void parseOccupantZoneConfigsLocked() { final Resources res = mContext.getResources(); // examples: // occupantZoneId=0,occupantType=DRIVER,seatRow=1,seatSide=driver // occupantZoneId=1,occupantType=FRONT_PASSENGER,seatRow=1, // searSide=oppositeDriver 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("Unrecognized 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); } // No zones defined. Then populate driver zone. if (maxZoneId == OccupantZoneInfo.INVALID_ZONE_ID) { maxZoneId++; mDriverZoneId = maxZoneId; Slogf.w(TAG, "No zones defined, add one as driver:%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); } @GuardedBy("mLock") private void parseDisplayConfigsLocked() { final Resources res = mContext.getResources(); final SparseArray inputTypesPerDisplay = new SparseArray<>(); // examples: // displayPort=0,displayType=MAIN,occupantZoneId=0,inputTypes=DPAD_KEYS| // NAVIGATE_KEYS|ROTARY_NAVIGATION // displayPort=1,displayType=INSTRUMENT_CLUSTER,occupantZoneId=0, // inputTypes=DPAD_KEYS 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; case "AUXILIARY_2": type = CarOccupantZoneManager.DISPLAY_TYPE_AUXILIARY_2; break; case "AUXILIARY_3": type = CarOccupantZoneManager.DISPLAY_TYPE_AUXILIARY_3; break; case "AUXILIARY_4": type = CarOccupantZoneManager.DISPLAY_TYPE_AUXILIARY_4; break; case "AUXILIARY_5": type = CarOccupantZoneManager.DISPLAY_TYPE_AUXILIARY_5; break; case "DISPLAY_COMPATIBILITY": type = CarOccupantZoneManager.DISPLAY_TYPE_DISPLAY_COMPATIBILITY; break; default: throwFormatErrorInDisplayMapping( "Unrecognized display type:" + entry); break; } inputTypesPerDisplay.set(type, new IntArray()); break; case "occupantZoneId": zoneId = Integer.parseInt(keyValuePair[1]); break; case "inputTypes": String[] inputStrings = keyValuePair[1].split("\\|"); for (int i = 0; i < inputStrings.length; i++) { switch (inputStrings[i].trim()) { case "ROTARY_NAVIGATION": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION); break; case "ROTARY_VOLUME": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_ROTARY_VOLUME); break; case "DPAD_KEYS": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_DPAD_KEYS); break; case "NAVIGATE_KEYS": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_NAVIGATE_KEYS); break; case "SYSTEM_NAVIGATE_KEYS": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS); break; case "CUSTOM_INPUT_EVENT": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT); break; case "TOUCH_SCREEN": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_TOUCH_SCREEN); break; case "NONE": inputTypesPerDisplay.get(type).add( CarInputManager.INPUT_TYPE_NONE); break; default: throw new IllegalArgumentException("Invalid input type: " + inputStrings[i]); } } break; default: throwFormatErrorInDisplayMapping("Unrecognized key:" + entry); break; } } // Now check validity checkInputTypeNoneLocked(inputTypesPerDisplay); 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); } final DisplayConfig displayConfig = new DisplayConfig(type, zoneId, inputTypesPerDisplay.get(type)); 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); } } Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); if (findDisplayConfigForDisplayLocked(defaultDisplay) == null) { int zoneForDefaultDisplay = mDriverZoneId; if (zoneForDefaultDisplay == OccupantZoneInfo.INVALID_ZONE_ID) { // No driver zone but we still need to allocate the default display to the 1st zone, // zone id 0. zoneForDefaultDisplay = 0; } Slogf.w(TAG, "No default display configuration, will assign to zone:" + zoneForDefaultDisplay); mDisplayUniqueIdConfigs.put(DisplayHelper.getUniqueId(defaultDisplay), new DisplayConfig(CarOccupantZoneManager.DISPLAY_TYPE_MAIN, zoneForDefaultDisplay, inputTypesPerDisplay.get( CarOccupantZoneManager.DISPLAY_TYPE_MAIN))); } } @GuardedBy("mLock") private void checkInputTypeNoneLocked(SparseArray inputTypesPerDisplay) { for (int i = 0; i < inputTypesPerDisplay.size(); ++i) { IntArray inputTypes = inputTypesPerDisplay.valueAt(i); for (int j = 0; j < inputTypes.size(); ++j) { if (inputTypes.get(j) == CarInputManager.INPUT_TYPE_NONE && inputTypes.size() > 1) { throw new IllegalArgumentException("Display {" + inputTypesPerDisplay.keyAt(i) + "} has input type NONE defined along with other input types"); } } } } @GuardedBy("mLock") 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); } @GuardedBy("mLock") private void handleActiveDisplaysLocked() { // Clear display info for each zone in preparation for re-populating display info. for (int i = 0; i < mActiveOccupantConfigs.size(); i++) { OccupantConfig occupantConfig = mActiveOccupantConfigs.valueAt(i); occupantConfig.displayInfos.clear(); } boolean hasDefaultDisplayConfig = false; boolean hasDriverZone = hasDriverZone(); 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 (hasDriverZone && 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)); } // Remove OccupantConfig in zones without displays. for (int i = 0; i < mActiveOccupantConfigs.size(); i++) { OccupantConfig occupantConfig = mActiveOccupantConfigs.valueAt(i); if (occupantConfig.displayInfos.size() == 0) { if (DBG) { Slogf.d(TAG, "handleActiveDisplaysLocked: removing zone %d due to no display", mActiveOccupantConfigs.keyAt(i)); } mActiveOccupantConfigs.removeAt(i); } } if (hasDriverZone && !hasDefaultDisplayConfig) { // This shouldn't happen, since we added the default display config in // parseDisplayConfigsLocked(). throw new IllegalStateException("Default display not assigned"); } } @VisibleForTesting int getCurrentUser() { return ActivityManager.getCurrentUser(); } @GuardedBy("mLock") private void updateEnabledProfilesLocked(@UserIdInt int userId) { mProfileUsers.clear(); List profileUsers = mUserHandleHelper.getEnabledProfiles(userId); for (UserHandle profiles : profileUsers) { if (profiles.getIdentifier() != userId) { mProfileUsers.add(profiles.getIdentifier()); } } } /** Returns {@code true} if user allocation has changed */ @GuardedBy("mLock") private boolean handleUserChangesLocked() { int currentUserId = getCurrentUser(); if (mEnableProfileUserAssignmentForMultiDisplay) { updateEnabledProfilesLocked(currentUserId); } boolean changed = false; for (int i = 0; i < mActiveOccupantConfigs.size(); ++i) { int zoneId = mActiveOccupantConfigs.keyAt(i); OccupantConfig config = mActiveOccupantConfigs.valueAt(i); OccupantZoneInfo info = mOccupantsConfig.get(zoneId); // Assign the current user to the driver zone if there is a driver zone. if (info.occupantType == CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER && config.userId != currentUserId) { config.userId = currentUserId; changed = true; if (DBG) { Slogf.d(TAG, "Changed driver, current user change to %d", currentUserId); } continue; } // Do not touch if the zone is un-assigned. if (config.userId == CarOccupantZoneManager.INVALID_USER_ID) { continue; } // Now it will be non-driver valid user id. if (!mCarUserService.isUserVisible(config.userId)) { if (DBG) Slogf.d(TAG, "Unassigned no longer visible user:%d", config.userId); config.userId = CarOccupantZoneManager.INVALID_USER_ID; changed = true; } } return changed; } @GuardedBy("mLock") 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(updateDisplay, updateUser); // Schedule remote callback invocation with the handler attached to the same Looper to // ensure that only one broadcast can be active at one time. mHandler.post(() -> { 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() { boolean changed; synchronized (mLock) { changed = handleUserChangesLocked(); } if (changed) { sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER); } } private void handlePassengerStarted() { sendConfigChangeEvent(CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER); } private void handlePassengerStopped() { 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); } } /** * Returns the supported input types for the occupant zone info and display type passed as * the argument. * * @param occupantZoneId the occupant zone id of the supported input types to find * @param displayType the display type of the supported input types to find * @return the supported input types for the occupant zone info and display type passed in as * the argument */ public int[] getSupportedInputTypes(int occupantZoneId, int displayType) { checkOccupantZone(occupantZoneId, displayType); synchronized (mLock) { // Search input type in mDisplayPortConfigs for (int i = 0; i < mDisplayPortConfigs.size(); i++) { DisplayConfig config = mDisplayPortConfigs.valueAt(i); if (config.displayType == displayType && config.occupantZoneId == occupantZoneId) { return config.inputTypes; } } // Search input type in mDisplayUniqueIdConfigs for (int i = 0; i < mDisplayUniqueIdConfigs.size(); i++) { DisplayConfig config = mDisplayUniqueIdConfigs.valueAt(i); if (config.displayType == displayType && config.occupantZoneId == occupantZoneId) { return config.inputTypes; } } } return EMPTY_INPUT_SUPPORT_TYPES; } private void checkOccupantZone(int occupantZoneId, int displayType) { if (Display.INVALID_DISPLAY == getDisplayForOccupant(occupantZoneId, displayType)) { throw new IllegalArgumentException("No display is associated with OccupantZoneInfo " + occupantZoneId); } } }