/* * 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 ArrayListThe 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