/* * 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.server.wm; import static com.android.server.wm.ActivityStarter.Request; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.Looper; import android.os.ServiceSpecificException; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseIntArray; import android.view.Display; import android.window.DisplayAreaOrganizer; import android.window.WindowContainerToken; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Class to control the assignment of a display for Car while launching a Activity. * *

This one controls which displays users are allowed to launch. * The policy should be passed from car service through * {@link com.android.internal.car.ICarServiceHelper} binder interfaces. If no policy is set, * this module will not change anything for launch process.

* *

The policy can only affect which display passenger users can use. Current user, assumed * to be a driver user, is allowed to launch any display always.

*/ public final class CarLaunchParamsModifier implements LaunchParamsController.LaunchParamsModifier { private static final String TAG = "CAR.LAUNCH"; private static final boolean DBG = false; // The following constants come from android.car.app.CarActivityManager. /** Indicates that the operation was successful. */ @VisibleForTesting static final int RESULT_SUCCESS = 0; /** * Internal error code for throwing {@link ActivityNotFoundException} from service. * @hide */ public static final int ERROR_CODE_ACTIVITY_NOT_FOUND = -101; private final Context mContext; private DisplayManager mDisplayManager; // set only from init() private ActivityTaskManagerService mAtm; // set only from init() private final Object mLock = new Object(); // Always start with USER_SYSTEM as the timing of handleCurrentUserSwitching(USER_SYSTEM) is not // guaranteed to be earler than 1st Activity launch. @GuardedBy("mLock") private int mCurrentDriverUser = UserHandle.USER_SYSTEM; // TODO: Switch from tracking displays to tracking display areas instead /** * This one is for holding all passenger (=profile user) displays which are mostly static unless * displays are added / removed. Note that {@link #mDisplayToProfileUserMapping} can be empty * while user is assigned and that cannot always tell if specific display is for driver or not. */ @GuardedBy("mLock") private final ArrayList mPassengerDisplays = new ArrayList<>(); /** key: display id, value: profile user id */ @GuardedBy("mLock") private final SparseIntArray mDisplayToProfileUserMapping = new SparseIntArray(); /** key: profile user id, value: display id */ @GuardedBy("mLock") private final SparseIntArray mDefaultDisplayForProfileUser = new SparseIntArray(); @GuardedBy("mLock") private boolean mIsSourcePreferred; @GuardedBy("mLock") private List mSourcePreferredComponents; @GuardedBy("mLock") private final ArrayMap mPersistentActivities = new ArrayMap<>(); @VisibleForTesting final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) { // ignore. car service should update whiltelist. } @Override public void onDisplayRemoved(int displayId) { synchronized (mLock) { mPassengerDisplays.remove(Integer.valueOf(displayId)); updateProfileUserConfigForDisplayRemovalLocked(displayId); } } @Override public void onDisplayChanged(int displayId) { // ignore } }; private void updateProfileUserConfigForDisplayRemovalLocked(int displayId) { mDisplayToProfileUserMapping.delete(displayId); int i = mDefaultDisplayForProfileUser.indexOfValue(displayId); if (i >= 0) { mDefaultDisplayForProfileUser.removeAt(i); } } /** Constructor. Can be constructed any time. */ public CarLaunchParamsModifier(Context context) { // This can be very early stage. So postpone interaction with other system until init. mContext = context; } /** * Initializes all internal stuffs. This should be called only after ATMS, DisplayManagerService * are ready. */ public void init() { mAtm = (ActivityTaskManagerService) ActivityTaskManager.getService(); LaunchParamsController controller = mAtm.mTaskSupervisor.getLaunchParamsController(); controller.registerModifier(this); mDisplayManager = mContext.getSystemService(DisplayManager.class); mDisplayManager.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper())); } /** * Sets sourcePreferred configuration. When sourcePreferred is enabled and there is no pre- * assigned display for the Activity, CarLauncherParamsModifier will launch the Activity in * the display of the source. When sourcePreferredComponents isn't null the sourcePreferred * is applied for the sourcePreferredComponents only. * * @param enableSourcePreferred whether to enable sourcePreferred mode * @param sourcePreferredComponents null for all components, or the list of components to apply */ public void setSourcePreferredComponents(boolean enableSourcePreferred, @Nullable List sourcePreferredComponents) { synchronized (mLock) { mIsSourcePreferred = enableSourcePreferred; mSourcePreferredComponents = sourcePreferredComponents; if (mSourcePreferredComponents != null) { Collections.sort(mSourcePreferredComponents); } } } /** Notifies user switching. */ public void handleCurrentUserSwitching(int newUserId) { synchronized (mLock) { mCurrentDriverUser = newUserId; mDefaultDisplayForProfileUser.clear(); mDisplayToProfileUserMapping.clear(); } } private void removeUserFromAllowlistsLocked(int userId) { for (int i = mDisplayToProfileUserMapping.size() - 1; i >= 0; i--) { if (mDisplayToProfileUserMapping.valueAt(i) == userId) { mDisplayToProfileUserMapping.removeAt(i); } } mDefaultDisplayForProfileUser.delete(userId); } /** Notifies user stopped. */ public void handleUserStopped(int stoppedUser) { // Note that the current user is never stopped. It always takes switching into // non-current user before stopping the user. synchronized (mLock) { removeUserFromAllowlistsLocked(stoppedUser); } } /** * Sets display allowlist for the userId. For passenger user, activity will be always launched * to a display in the allowlist. If requested display is not in the allowlist, the 1st display * in the allowlist will be selected as target display. * *

The allowlist is kept only for profile user. Assigning the current user unassigns users * for the given displays. */ public void setDisplayAllowListForUser(int userId, int[] displayIds) { if (DBG) { Slog.d(TAG, "setDisplayAllowlistForUser userId:" + userId + " displays:" + displayIds); } synchronized (mLock) { for (int displayId : displayIds) { if (!mPassengerDisplays.contains(displayId)) { Slog.w(TAG, "setDisplayAllowlistForUser called with display:" + displayId + " not in passenger display list:" + mPassengerDisplays); continue; } if (userId == mCurrentDriverUser) { mDisplayToProfileUserMapping.delete(displayId); } else { mDisplayToProfileUserMapping.put(displayId, userId); } // now the display cannot be a default display for other user int i = mDefaultDisplayForProfileUser.indexOfValue(displayId); if (i >= 0) { mDefaultDisplayForProfileUser.removeAt(i); } } if (displayIds.length > 0) { mDefaultDisplayForProfileUser.put(userId, displayIds[0]); } else { removeUserFromAllowlistsLocked(userId); } } } /** * Sets displays assigned to passenger. All other displays will be treated as assigned to * driver. * *

The 1st display in the array will be considered as a default display to assign * for any non-driver user if there is no display assigned for the user.

*/ public void setPassengerDisplays(int[] displayIdsForPassenger) { if (DBG) { Slog.d(TAG, "setPassengerDisplays displays:" + displayIdsForPassenger); } synchronized (mLock) { for (int id : displayIdsForPassenger) { mPassengerDisplays.remove(Integer.valueOf(id)); } // handle removed displays for (int i = 0; i < mPassengerDisplays.size(); i++) { int displayId = mPassengerDisplays.get(i); updateProfileUserConfigForDisplayRemovalLocked(displayId); } mPassengerDisplays.clear(); mPassengerDisplays.ensureCapacity(displayIdsForPassenger.length); for (int id : displayIdsForPassenger) { mPassengerDisplays.add(id); } } } /** * Decides display to assign while an Activity is launched. * *

For current user (=driver), launching to any display is allowed as long as system * allows it.

* *

For private display, do not change anything as private display has its own logic.

* *

For passenger displays, only run in allowed displays. If requested display is not * allowed, change to the 1st allowed display.

*/ @Override @Result public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout, @Nullable ActivityRecord activity, @Nullable ActivityRecord source, ActivityOptions options, @Nullable Request request, int phase, LaunchParamsController.LaunchParams currentParams, LaunchParamsController.LaunchParams outParams) { int userId; if (task != null) { userId = task.mUserId; } else if (activity != null) { userId = activity.mUserId; } else { Slog.w(TAG, "onCalculate, cannot decide user"); return RESULT_SKIP; } // DisplayArea where user wants to launch the Activity. TaskDisplayArea originalDisplayArea = currentParams.mPreferredTaskDisplayArea; // DisplayArea where CarLaunchParamsModifier targets to launch the Activity. TaskDisplayArea targetDisplayArea = null; if (DBG) { Slog.d(TAG, "onCalculate, userId:" + userId + " original displayArea:" + originalDisplayArea + " ActivityOptions:" + options); } ComponentName activityName = null; if (activity != null && activity.info != null) { activityName = activity.info.getComponentName(); } decision: synchronized (mLock) { // If originalDisplayArea is set, respect that before ActivityOptions check. if (originalDisplayArea == null) { if (options != null) { WindowContainerToken daToken = options.getLaunchTaskDisplayArea(); if (daToken != null) { originalDisplayArea = (TaskDisplayArea) WindowContainer.fromBinder( daToken.asBinder()); } else { int originalDisplayId = options.getLaunchDisplayId(); if (originalDisplayId != Display.INVALID_DISPLAY) { originalDisplayArea = getDefaultTaskDisplayAreaOnDisplay( originalDisplayId); } } } } if (mPersistentActivities.containsKey(activityName)) { targetDisplayArea = mPersistentActivities.get(activityName); } else if (originalDisplayArea == null // No specified DA to launch the Activity && mIsSourcePreferred && source != null && (mSourcePreferredComponents == null || Collections.binarySearch( mSourcePreferredComponents, activity.info.getComponentName()) >= 0)) { targetDisplayArea = source.noDisplay ? source.mHandoverTaskDisplayArea : source.getDisplayArea(); } else if (originalDisplayArea == null && task == null // launching as a new task && source != null && !source.getDisplayContent().isTrusted() && ((activity.info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0)) { if (DBG) { Slog.d(TAG, "Disallow launch on virtual display for not-embedded activity."); } targetDisplayArea = getDefaultTaskDisplayAreaOnDisplay(Display.DEFAULT_DISPLAY); } if (userId == mCurrentDriverUser) { // Respect the existing DisplayArea. break decision; } if (userId == UserHandle.USER_SYSTEM) { // This will be only allowed if it has FLAG_SHOW_FOR_ALL_USERS. // The flag is not immediately accessible here so skip the check. // But other WM policy will enforce it. break decision; } // Now user is a passenger. if (mPassengerDisplays.isEmpty()) { // No displays for passengers. This could be old user and do not do anything. break decision; } if (targetDisplayArea == null) { if (originalDisplayArea != null) { targetDisplayArea = originalDisplayArea; } else { targetDisplayArea = getDefaultTaskDisplayAreaOnDisplay(Display.DEFAULT_DISPLAY); } } Display display = targetDisplayArea.mDisplayContent.getDisplay(); if ((display.getFlags() & Display.FLAG_PRIVATE) != 0) { // private display should follow its own restriction rule. break decision; } if (display.getType() == Display.TYPE_VIRTUAL) { // TODO(b/132903422) : We need to update this after the bug is resolved. // For now, don't change anything. break decision; } int userForDisplay = mDisplayToProfileUserMapping.get(display.getDisplayId(), UserHandle.USER_NULL); if (userForDisplay == userId) { break decision; } targetDisplayArea = getAlternativeDisplayAreaForPassengerLocked( userId, activity, request); } if (targetDisplayArea != null && originalDisplayArea != targetDisplayArea) { Slog.i(TAG, "Changed launching display, user:" + userId + " requested display area:" + originalDisplayArea + " target display area:" + targetDisplayArea); outParams.mPreferredTaskDisplayArea = targetDisplayArea; return RESULT_DONE; } else { return RESULT_SKIP; } } @Nullable private TaskDisplayArea getAlternativeDisplayAreaForPassengerLocked(int userId, @NonNull ActivityRecord activityRecord, @Nullable Request request) { TaskDisplayArea sourceDisplayArea = sourceDisplayArea(userId, activityRecord, request); return sourceDisplayArea != null ? sourceDisplayArea : fallbackDisplayArea(userId); } @VisibleForTesting @Nullable TaskDisplayArea getDefaultTaskDisplayAreaOnDisplay(int displayId) { DisplayContent dc = mAtm.mRootWindowContainer.getDisplayContentOrCreate(displayId); if (dc == null) { return null; } return dc.getDefaultTaskDisplayArea(); } /** * Calculates the {@link TaskDisplayArea} for the source of the request. The source is * calculated implicitly from the request or the activity record. * * @param userId ID of the current active user * @param activityRecord {@link ActivityRecord} that is to be shown * @param request {@link Request} data for showing the {@link ActivityRecord} * @return {@link TaskDisplayArea} First non {@code null} candidate display area that is allowed * for the user. It is allowed if the display has been added to the profile mapping. */ @Nullable private TaskDisplayArea sourceDisplayArea(int userId, @NonNull ActivityRecord activityRecord, @Nullable Request request) { List candidateControllers = candidateControllers(activityRecord, request); for (int i = 0; i < candidateControllers.size(); i++) { WindowProcessController controller = candidateControllers.get(i); TaskDisplayArea candidate = controller.getTopActivityDisplayArea(); int displayId = candidate != null ? candidate.getDisplayId() : Display.INVALID_DISPLAY; int userForDisplay = mDisplayToProfileUserMapping.get(displayId, UserHandle.USER_NULL); if (userForDisplay == userId) { return candidate; } } return null; } /** * Calculates a list of {@link WindowProcessController} that can calculate the * {@link TaskDisplayArea} to house the {@link ActivityRecord}. Controllers are calculated since * calculating the display can be expensive. The list is ordered in the * following way *
    *
  1. Controller for the activity record from the process name and app uid
  2. *
  3. Controller for the activity that is launching the given record
  4. *
  5. Controller for the actual process that is launching the record
  6. *
* * @param activityRecord {@link ActivityRecord} that is to be shown * @param request {@link Request} data for showing the {@link ActivityRecord} * @return {@link List} of {@link WindowProcessController} ordered by preference to be shown */ private List candidateControllers( @NonNull ActivityRecord activityRecord, @Nullable Request request) { WindowProcessController firstController = mAtm.getProcessController( activityRecord.getProcessName(), activityRecord.getUid()); WindowProcessController secondController = mAtm.getProcessController( activityRecord.getLaunchedFromPid(), activityRecord.getLaunchedFromUid()); WindowProcessController thirdController = request == null ? null : mAtm.getProcessController(request.realCallingPid, request.realCallingUid); List candidates = new ArrayList<>(3); if (firstController != null) { candidates.add(firstController); } if (secondController != null) { candidates.add(secondController); } if (thirdController != null) { candidates.add(thirdController); } return candidates; } /** * Return a {@link TaskDisplayArea} that can be used if a source display area is not found. * First check the default display for the user. If it is absent select the first passenger * display if present. If both are absent return {@code null} * * @param userId ID of the active user * @return {@link TaskDisplayArea} that is recommended when a display area is not specified */ @Nullable private TaskDisplayArea fallbackDisplayArea(int userId) { int displayIdForUserProfile = mDefaultDisplayForProfileUser.get(userId, Display.INVALID_DISPLAY); if (displayIdForUserProfile != Display.INVALID_DISPLAY) { int displayId = mDefaultDisplayForProfileUser.get(userId); return getDefaultTaskDisplayAreaOnDisplay(displayId); } if (!mPassengerDisplays.isEmpty()) { int displayId = mPassengerDisplays.get(0); return getDefaultTaskDisplayAreaOnDisplay(displayId); } return null; } private TaskDisplayArea findTaskDisplayArea(DisplayContent display, int featureId) { return display.getItemFromTaskDisplayAreas( displayArea -> displayArea.mFeatureId == featureId ? displayArea : null); } /** * See {@code CarActivityManager#setPersistentActivity(android.content.ComponentName,int, int)} */ public int setPersistentActivity(ComponentName activity, int displayId, int featureId) { if (DBG) { Slog.d(TAG, "setPersistentActivity: activity=" + activity + ", displayId=" + displayId + ", featureId=" + featureId); } if (featureId == DisplayAreaOrganizer.FEATURE_UNDEFINED) { synchronized (mLock) { TaskDisplayArea removed = mPersistentActivities.remove(activity); if (removed == null) { throw new ServiceSpecificException( ERROR_CODE_ACTIVITY_NOT_FOUND, "Failed to remove " + activity.toShortString()); } return RESULT_SUCCESS; } } DisplayContent display = mAtm.mRootWindowContainer.getDisplayContentOrCreate(displayId); if (display == null) { throw new IllegalArgumentException("Unknown display=" + displayId); } TaskDisplayArea tda = findTaskDisplayArea(display, featureId); if (tda == null) { throw new IllegalArgumentException("Unknown feature=" + featureId); } synchronized (mLock) { mPersistentActivities.put(activity, tda); } return RESULT_SUCCESS; } }