diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2018-11-12 22:01:31 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2018-11-12 22:01:31 +0000 |
commit | 581cccdde99adfae7d05b51fa86acdb83d80b1bf (patch) | |
tree | a1d30379b3a03b2701d5d02e219c35beab1d7c45 | |
parent | a79fac052f6e8027c829fbb3cd4bfd9e7ce3bbe0 (diff) | |
parent | 891146c6a6e40f40d9e9ca34378389a5aa6a53a7 (diff) |
Merge "Implement launch bounds logic in Android (3/3)"
16 files changed, 1375 insertions, 77 deletions
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 3f14379c9bc9..4ead34ed2f97 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -455,6 +455,19 @@ public final class Display { } /** + * Gets the display unique id. + * <p> + * Unique id is different from display id because physical displays have stable unique id across + * reboots. + * + * @see com.android.service.display.DisplayDevice#hasStableUniqueId(). + * @hide + */ + public String getUniqueId() { + return mDisplayInfo.uniqueId; + } + + /** * Returns true if this display is still valid, false if the display has been removed. * * If the display is invalid, then the methods of this class will diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index 1944184319a3..7fcee3db3e78 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -5184,12 +5184,14 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai /** * Removes the input task from this stack. + * * @param task to remove. * @param reason for removal. * @param mode task removal mode. Either {@link #REMOVE_TASK_MODE_DESTROYING}, * {@link #REMOVE_TASK_MODE_MOVING}, {@link #REMOVE_TASK_MODE_MOVING_TO_TOP}. */ void removeTask(TaskRecord task, String reason, int mode) { + // TODO(b/119259346): Move some logic below to TaskRecord. See bug for more context. for (ActivityRecord record : task.mActivities) { onActivityRemovedFromStack(record); } @@ -5204,6 +5206,9 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai updateTaskMovement(task, true); if (mode == REMOVE_TASK_MODE_DESTROYING && task.mActivities.isEmpty()) { + // This task is going away, so save the last state if necessary. + task.saveLaunchingStateIfNeeded(); + // TODO: VI what about activity? final boolean isVoiceSession = task.voiceSession != null; if (isVoiceSession) { diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index 151f6f787bd1..97eaafcdc622 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -174,6 +174,7 @@ import android.util.SparseIntArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import android.view.Display; +import android.view.DisplayInfo; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -327,6 +328,9 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D WindowManagerService mWindowManager; DisplayManager mDisplayManager; + /** Common synchronization logic used to save things to disks. */ + PersisterQueue mPersisterQueue; + LaunchParamsPersister mLaunchParamsPersister; private LaunchParamsController mLaunchParamsController; /** @@ -631,10 +635,16 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D mActivityMetricsLogger = new ActivityMetricsLogger(this, mService.mContext, mHandler.getLooper()); mKeyguardController = new KeyguardController(mService, this); - mLaunchParamsController = new LaunchParamsController(mService); + mPersisterQueue = new PersisterQueue(); + mLaunchParamsPersister = new LaunchParamsPersister(mPersisterQueue, this); + mLaunchParamsController = new LaunchParamsController(mService, mLaunchParamsPersister); mLaunchParamsController.registerDefaultModifiers(this); } + void onSystemReady() { + mPersisterQueue.startPersisting(); + mLaunchParamsPersister.onSystemReady(); + } public ActivityMetricsLogger getActivityMetricsLogger() { return mActivityMetricsLogger; @@ -4249,6 +4259,25 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D return activityDisplay; } + /** + * Get an existing instance of {@link ActivityDisplay} that has the given uniqueId. Unique ID is + * defined in {@link DisplayInfo#uniqueId}. + * + * @param uniqueId the unique ID of the display + * @return the {@link ActivityDisplay} or {@code null} if nothing is found. + */ + ActivityDisplay getActivityDisplay(String uniqueId) { + for (int i = mActivityDisplays.size() - 1; i >= 0; --i) { + final ActivityDisplay display = mActivityDisplays.get(i); + final boolean isValid = display.mDisplay.isValid(); + if (isValid && display.mDisplay.getUniqueId().equals(uniqueId)) { + return display; + } + } + + return null; + } + boolean startHomeOnAllDisplays(int userId, String reason) { boolean homeStarted = false; for (int i = mActivityDisplays.size() - 1; i >= 0; i--) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 4c5969c76fa6..1d0007545d78 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -653,6 +653,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAssistUtils = new AssistUtils(mContext); mVrController.onSystemReady(); mRecentTasks.onSystemReadyLocked(); + mStackSupervisor.onSystemReady(); } } @@ -910,6 +911,20 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mService.start(); } + @Override + public void onUnlockUser(int userId) { + synchronized (mService.getGlobalLock()) { + mService.mStackSupervisor.mLaunchParamsPersister.onUnlockUser(userId); + } + } + + @Override + public void onCleanupUser(int userId) { + synchronized (mService.getGlobalLock()) { + mService.mStackSupervisor.mLaunchParamsPersister.onCleanupUser(userId); + } + } + public ActivityTaskManagerService getService() { return mService; } diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java index 252f47ca16e8..09475777cb6e 100644 --- a/services/core/java/com/android/server/wm/LaunchParamsController.java +++ b/services/core/java/com/android/server/wm/LaunchParamsController.java @@ -40,6 +40,7 @@ import java.util.List; */ class LaunchParamsController { private final ActivityTaskManagerService mService; + private final LaunchParamsPersister mPersister; private final List<LaunchParamsModifier> mModifiers = new ArrayList<>(); // Temporary {@link LaunchParams} for internal calculations. This is kept separate from @@ -49,8 +50,9 @@ class LaunchParamsController { private final LaunchParams mTmpCurrent = new LaunchParams(); private final LaunchParams mTmpResult = new LaunchParams(); - LaunchParamsController(ActivityTaskManagerService service) { - mService = service; + LaunchParamsController(ActivityTaskManagerService service, LaunchParamsPersister persister) { + mService = service; + mPersister = persister; } /** @@ -75,6 +77,10 @@ class LaunchParamsController { ActivityRecord source, ActivityOptions options, LaunchParams result) { result.reset(); + if (task != null || activity != null) { + mPersister.getLaunchParams(task, activity, result); + } + // We start at the last registered {@link LaunchParamsModifier} as this represents // The modifier closest to the product level. Moving back through the list moves closer to // the platform logic. @@ -139,12 +145,20 @@ class LaunchParamsController { task.getStack().setWindowingMode(mTmpParams.mWindowingMode); } - if (!mTmpParams.mBounds.isEmpty()) { + if (mTmpParams.mBounds.isEmpty()) { + return false; + } + + if (task.getStack().inFreeformWindowingMode()) { + // Only set bounds if it's in freeform mode. task.updateOverrideConfiguration(mTmpParams.mBounds); return true; - } else { - return false; } + + // Setting last non-fullscreen bounds to the bounds so next time the task enters + // freeform windowing mode it can be in this bounds. + task.setLastNonFullscreenBounds(mTmpParams.mBounds); + return false; } finally { mService.mWindowManager.continueSurfaceLayout(); } diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java new file mode 100644 index 000000000000..72d51439d9f7 --- /dev/null +++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2018 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 android.content.ComponentName; +import android.content.pm.PackageList; +import android.content.pm.PackageManagerInternal; +import android.graphics.Rect; +import android.os.Environment; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.SparseArray; +import android.util.Xml; +import android.view.DisplayInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastXmlSerializer; +import com.android.server.LocalServices; +import com.android.server.wm.LaunchParamsController.LaunchParams; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntFunction; + +/** + * Persister that saves launch parameters in memory and in storage. It saves the last seen state of + * tasks key-ed on task's user ID and the activity used to launch the task ({@link + * TaskRecord#realActivity}) and that's used to determine the launch params when the activity is + * being launched again in {@link LaunchParamsController}. + * + * Need to hold {@link ActivityTaskManagerService#getGlobalLock()} to access this class. + */ +class LaunchParamsPersister { + private static final String TAG = "LaunchParamsPersister"; + private static final String LAUNCH_PARAMS_DIRNAME = "launch_params"; + private static final String LAUNCH_PARAMS_FILE_SUFFIX = ".xml"; + + // Chars below are used to escape the backslash in component name to underscore. + private static final char ORIGINAL_COMPONENT_SEPARATOR = '/'; + private static final char ESCAPED_COMPONENT_SEPARATOR = '_'; + + private static final String TAG_LAUNCH_PARAMS = "launch_params"; + + private final PersisterQueue mPersisterQueue; + private final ActivityStackSupervisor mSupervisor; + + /** + * A function that takes in user ID and returns a folder to store information of that user. Used + * to differentiate storage location in test environment and production environment. + */ + private final IntFunction<File> mUserFolderGetter; + + private PackageList mPackageList; + + /** + * A dual layer map that first maps user ID to a secondary map, which maps component name (the + * launching activity of tasks) to {@link PersistableLaunchParams} that stores launch metadata + * that are stable across reboots. + */ + private final SparseArray<ArrayMap<ComponentName, PersistableLaunchParams>> mMap = + new SparseArray<>(); + + LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor) { + this(persisterQueue, supervisor, Environment::getDataSystemCeDirectory); + } + + @VisibleForTesting + LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor, + IntFunction<File> userFolderGetter) { + mPersisterQueue = persisterQueue; + mSupervisor = supervisor; + mUserFolderGetter = userFolderGetter; + } + + void onSystemReady() { + PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + mPackageList = pmi.getPackageList(new PackageListObserver()); + } + + void onUnlockUser(int userId) { + loadLaunchParams(userId); + } + + void onCleanupUser(int userId) { + mMap.remove(userId); + } + + private void loadLaunchParams(int userId) { + final List<File> filesToDelete = new ArrayList<>(); + final File launchParamsFolder = getLaunchParamFolder(userId); + if (!launchParamsFolder.isDirectory()) { + Slog.i(TAG, "Didn't find launch param folder for user " + userId); + return; + } + + final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames()); + + final File[] paramsFiles = launchParamsFolder.listFiles(); + final ArrayMap<ComponentName, PersistableLaunchParams> map = + new ArrayMap<>(paramsFiles.length); + mMap.put(userId, map); + + for (File paramsFile : paramsFiles) { + if (!paramsFile.isFile()) { + Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file."); + continue; + } + if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) { + Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName()); + filesToDelete.add(paramsFile); + continue; + } + final String paramsFileName = paramsFile.getName(); + final String componentNameString = paramsFileName.substring( + 0 /* beginIndex */, + paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length()) + .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR); + final ComponentName name = ComponentName.unflattenFromString( + componentNameString); + if (name == null) { + Slog.w(TAG, "Unexpected file name: " + paramsFileName); + filesToDelete.add(paramsFile); + continue; + } + + if (!packages.contains(name.getPackageName())) { + // Rare case. PersisterQueue doesn't have a chance to remove files for removed + // packages last time. + filesToDelete.add(paramsFile); + continue; + } + + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(paramsFile)); + final PersistableLaunchParams params = new PersistableLaunchParams(); + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(reader); + int event; + while ((event = parser.next()) != XmlPullParser.END_DOCUMENT + && event != XmlPullParser.END_TAG) { + if (event != XmlPullParser.START_TAG) { + continue; + } + + final String tagName = parser.getName(); + if (!TAG_LAUNCH_PARAMS.equals(tagName)) { + Slog.w(TAG, "Unexpected tag name: " + tagName); + continue; + } + + params.restoreFromXml(parser); + } + + map.put(name, params); + } catch (Exception e) { + Slog.w(TAG, "Failed to restore launch params for " + name, e); + filesToDelete.add(paramsFile); + } finally { + IoUtils.closeQuietly(reader); + } + } + + if (!filesToDelete.isEmpty()) { + mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true); + } + } + + void saveTask(TaskRecord task) { + final ComponentName name = task.realActivity; + final int userId = task.userId; + PersistableLaunchParams params; + ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.get(userId); + if (map == null) { + map = new ArrayMap<>(); + mMap.put(userId, map); + } + + params = map.get(name); + if (params == null) { + params = new PersistableLaunchParams(); + map.put(name, params); + } + final boolean changed = saveTaskToLaunchParam(task, params); + + if (changed) { + mPersisterQueue.updateLastOrAddItem( + new LaunchParamsWriteQueueItem(userId, name, params), + /* flush */ false); + } + } + + private boolean saveTaskToLaunchParam(TaskRecord task, PersistableLaunchParams params) { + final ActivityStack<?> stack = task.getStack(); + final int displayId = stack.mDisplayId; + final ActivityDisplay display = mSupervisor.getActivityDisplay(displayId); + final DisplayInfo info = new DisplayInfo(); + display.mDisplay.getDisplayInfo(info); + + boolean changed = !Objects.equals(params.mDisplayUniqueId, info.uniqueId); + params.mDisplayUniqueId = info.uniqueId; + + changed |= params.mWindowingMode != stack.getWindowingMode(); + params.mWindowingMode = stack.getWindowingMode(); + + if (task.mLastNonFullscreenBounds != null) { + changed |= !Objects.equals(params.mBounds, task.mLastNonFullscreenBounds); + params.mBounds.set(task.mLastNonFullscreenBounds); + } else { + changed |= !params.mBounds.isEmpty(); + params.mBounds.setEmpty(); + } + + return changed; + } + + void getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams outParams) { + final ComponentName name = task != null ? task.realActivity : activity.realActivity; + final int userId = task != null ? task.userId : activity.userId; + + outParams.reset(); + Map<ComponentName, PersistableLaunchParams> map = mMap.get(userId); + if (map == null) { + return; + } + final PersistableLaunchParams persistableParams = map.get(name); + + if (persistableParams == null) { + return; + } + + final ActivityDisplay display = mSupervisor.getActivityDisplay( + persistableParams.mDisplayUniqueId); + if (display != null) { + outParams.mPreferredDisplayId = display.mDisplayId; + } + outParams.mWindowingMode = persistableParams.mWindowingMode; + outParams.mBounds.set(persistableParams.mBounds); + } + + private void onPackageRemoved(String packageName) { + final List<File> fileToDelete = new ArrayList<>(); + for (int i = 0; i < mMap.size(); ++i) { + int userId = mMap.keyAt(i); + final File launchParamsFolder = getLaunchParamFolder(userId); + ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.valueAt(i); + for (int j = map.size() - 1; j >= 0; --j) { + final ComponentName name = map.keyAt(j); + if (name.getPackageName().equals(packageName)) { + map.removeAt(j); + fileToDelete.add(getParamFile(launchParamsFolder, name)); + } + } + } + + synchronized (mPersisterQueue) { + mPersisterQueue.removeItems( + item -> item.mComponentName.getPackageName().equals(packageName), + LaunchParamsWriteQueueItem.class); + + mPersisterQueue.addItem(new CleanUpComponentQueueItem(fileToDelete), true); + } + } + + private File getParamFile(File launchParamFolder, ComponentName name) { + final String componentNameString = name.flattenToShortString() + .replace(ORIGINAL_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR); + return new File(launchParamFolder, componentNameString + LAUNCH_PARAMS_FILE_SUFFIX); + } + + private File getLaunchParamFolder(int userId) { + final File userFolder = mUserFolderGetter.apply(userId); + return new File(userFolder, LAUNCH_PARAMS_DIRNAME); + } + + private class PackageListObserver implements PackageManagerInternal.PackageListObserver { + @Override + public void onPackageAdded(String packageName) { } + + @Override + public void onPackageRemoved(String packageName) { + LaunchParamsPersister.this.onPackageRemoved(packageName); + } + } + + private class LaunchParamsWriteQueueItem + implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> { + private final int mUserId; + private final ComponentName mComponentName; + + private PersistableLaunchParams mLaunchParams; + + private LaunchParamsWriteQueueItem(int userId, ComponentName componentName, + PersistableLaunchParams launchParams) { + mUserId = userId; + mComponentName = componentName; + mLaunchParams = launchParams; + } + + private StringWriter saveParamsToXml() { + final StringWriter writer = new StringWriter(); + final XmlSerializer serializer = new FastXmlSerializer(); + + try { + serializer.setOutput(writer); + serializer.startDocument(/* encoding */ null, /* standalone */ true); + serializer.startTag(null, TAG_LAUNCH_PARAMS); + + mLaunchParams.saveToXml(serializer); + + serializer.endTag(null, TAG_LAUNCH_PARAMS); + serializer.endDocument(); + serializer.flush(); + + return writer; + } catch (IOException e) { + return null; + } + } + + @Override + public void process() { + final StringWriter writer = saveParamsToXml(); + + final File launchParamFolder = getLaunchParamFolder(mUserId); + if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdirs()) { + Slog.w(TAG, "Failed to create folder for " + mUserId); + return; + } + + final File launchParamFile = getParamFile(launchParamFolder, mComponentName); + final AtomicFile atomicFile = new AtomicFile(launchParamFile); + + FileOutputStream stream = null; + try { + stream = atomicFile.startWrite(); + stream.write(writer.toString().getBytes()); + } catch (Exception e) { + Slog.e(TAG, "Failed to write param file for " + mComponentName, e); + if (stream != null) { + atomicFile.failWrite(stream); + } + return; + } + atomicFile.finishWrite(stream); + } + + @Override + public boolean matches(LaunchParamsWriteQueueItem item) { + return mUserId == item.mUserId && mComponentName.equals(item.mComponentName); + } + + @Override + public void updateFrom(LaunchParamsWriteQueueItem item) { + mLaunchParams = item.mLaunchParams; + } + } + + private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem { + private final List<File> mComponentFiles; + + private CleanUpComponentQueueItem(List<File> componentFiles) { + mComponentFiles = componentFiles; + } + + @Override + public void process() { + for (File file : mComponentFiles) { + if (!file.delete()) { + Slog.w(TAG, "Failed to delete " + file.getAbsolutePath()); + } + } + } + } + + private class PersistableLaunchParams { + private static final String ATTR_WINDOWING_MODE = "windowing_mode"; + private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id"; + private static final String ATTR_BOUNDS = "bounds"; + + /** The bounds within the parent container. */ + final Rect mBounds = new Rect(); + + /** The unique id of the display the {@link TaskRecord} would prefer to be on. */ + String mDisplayUniqueId; + + /** The windowing mode to be in. */ + int mWindowingMode; + + void saveToXml(XmlSerializer serializer) throws IOException { + serializer.attribute(null, ATTR_DISPLAY_UNIQUE_ID, mDisplayUniqueId); + serializer.attribute(null, ATTR_WINDOWING_MODE, + Integer.toString(mWindowingMode)); + serializer.attribute(null, ATTR_BOUNDS, mBounds.flattenToString()); + } + + void restoreFromXml(XmlPullParser parser) { + for (int i = 0; i < parser.getAttributeCount(); ++i) { + final String attrValue = parser.getAttributeValue(i); + switch (parser.getAttributeName(i)) { + case ATTR_DISPLAY_UNIQUE_ID: + mDisplayUniqueId = attrValue; + break; + case ATTR_WINDOWING_MODE: + mWindowingMode = Integer.parseInt(attrValue); + break; + case ATTR_BOUNDS: { + final Rect bounds = Rect.unflattenFromString(attrValue); + if (bounds != null) { + mBounds.set(bounds); + } + break; + } + } + } + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder("PersistableLaunchParams{"); + builder.append("windowingMode=" + mWindowingMode); + builder.append(" displayUniqueId=" + mDisplayUniqueId); + builder.append(" bounds=" + mBounds); + builder.append(" }"); + return builder.toString(); + } + } +} diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java index 1cfc7acbb3df..a17ee6581ea6 100644 --- a/services/core/java/com/android/server/wm/PersisterQueue.java +++ b/services/core/java/com/android/server/wm/PersisterQueue.java @@ -130,6 +130,30 @@ class PersisterQueue { return null; } + /** + * + * @param item + * @param flush + * @param <T> + */ + synchronized <T extends WriteQueueItem> void updateLastOrAddItem(T item, boolean flush) { + final T itemToUpdate = findLastItem(item::matches, (Class<T>) item.getClass()); + if (itemToUpdate == null) { + addItem(item, flush); + } else { + itemToUpdate.updateFrom(item); + } + + yieldIfQueueTooDeep(); + } + + /** + * Removes all items with which given predicate returns {@code true}. + * + * @param predicate the predicate + * @param clazz + * @param <T> + */ synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate, Class<T> clazz) { for (int i = mWriteQueue.size() - 1; i >= 0; --i) { @@ -230,8 +254,14 @@ class PersisterQueue { item.process(); } - interface WriteQueueItem { + interface WriteQueueItem<T extends WriteQueueItem<T>> { void process(); + + default void updateFrom(T item) {} + + default boolean matches(T item) { + return false; + } } interface Listener { diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index c995d3fcbcc4..15478b49c08e 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.FLAG_AND_UNLOCKED; import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; import static android.app.ActivityManager.RECENT_WITH_EXCLUDED; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; @@ -33,6 +34,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.Process.SYSTEM_UID; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.server.wm.ActivityStackSupervisor.REMOVE_FROM_RECENTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS_TRIM_TASKS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TASKS; @@ -40,8 +42,6 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECEN import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.ActivityStackSupervisor.REMOVE_FROM_RECENTS; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -196,7 +196,8 @@ class RecentTasks { final Resources res = service.mContext.getResources(); mService = service; mSupervisor = mService.mStackSupervisor; - mTaskPersister = new TaskPersister(systemDir, stackSupervisor, service, this); + mTaskPersister = new TaskPersister(systemDir, stackSupervisor, service, this, + stackSupervisor.mPersisterQueue); mGlobalMaxNumTasks = ActivityTaskManager.getMaxRecentTasksStatic(); mHasVisibleRecentTasks = res.getBoolean(com.android.internal.R.bool.config_hasRecents); loadParametersFromResources(res); @@ -432,7 +433,6 @@ class RecentTasks { void onSystemReadyLocked() { loadRecentsComponent(mService.mContext.getResources()); mTasks.clear(); - mTaskPersister.onSystemReady(); } Bitmap getTaskDescriptionIcon(String path) { diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java index b7804e8d13a8..117984af67e7 100644 --- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java @@ -48,6 +48,7 @@ import android.graphics.Rect; import android.os.Build; import android.util.Slog; import android.view.Gravity; +import android.view.View; import com.android.server.wm.LaunchParamsController.LaunchParams; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; @@ -198,13 +199,13 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { || displayId == currentParams.mPreferredDisplayId)) { if (currentParams.hasWindowingMode()) { launchMode = currentParams.mWindowingMode; - fullyResolvedCurrentParam = (launchMode != WINDOWING_MODE_FREEFORM); + fullyResolvedCurrentParam = launchMode != WINDOWING_MODE_FREEFORM; if (DEBUG) { appendLog("inherit-" + WindowConfiguration.windowingModeToString(launchMode)); } } - if (!currentParams.mBounds.isEmpty()) { + if (launchMode == WINDOWING_MODE_FREEFORM && !currentParams.mBounds.isEmpty()) { outParams.mBounds.set(currentParams.mBounds); fullyResolvedCurrentParam = true; if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds); @@ -250,11 +251,20 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { // for all other windowing modes that's not freeform mode. One can read comments in // relevant methods to further understand this step. // - // We skip making adjustments if the params are fully resolved from previous results and - // trust that they are valid. - if (!fullyResolvedCurrentParam) { - final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode - : display.getWindowingMode(); + // We skip making adjustments if the params are fully resolved from previous results. + final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode + : display.getWindowingMode(); + if (fullyResolvedCurrentParam) { + if (resolvedMode == WINDOWING_MODE_FREEFORM) { + // Make sure bounds are in the display if it's possibly in a different display. + if (currentParams.mPreferredDisplayId != displayId) { + adjustBoundsToFitInDisplay(display, outParams.mBounds); + } + // Even though we want to keep original bounds, we still don't want it to stomp on + // an existing task. + adjustBoundsToAvoidConflict(display, outParams.mBounds); + } + } else { if (source != null && source.inFreeformWindowingMode() && resolvedMode == WINDOWING_MODE_FREEFORM && outParams.mBounds.isEmpty() @@ -291,13 +301,12 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { } if (displayId != INVALID_DISPLAY && mSupervisor.getActivityDisplay(displayId) == null) { - displayId = INVALID_DISPLAY; + displayId = currentParams.mPreferredDisplayId; } displayId = (displayId == INVALID_DISPLAY) ? currentParams.mPreferredDisplayId : displayId; - displayId = (displayId == INVALID_DISPLAY) ? DEFAULT_DISPLAY : displayId; - - return displayId; + return (displayId != INVALID_DISPLAY && mSupervisor.getActivityDisplay(displayId) != null) + ? displayId : DEFAULT_DISPLAY; } private boolean canApplyFreeformWindowPolicy(@NonNull ActivityDisplay display, int launchMode) { @@ -596,7 +605,12 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { if (displayBounds.width() < inOutBounds.width() || displayBounds.height() < inOutBounds.height()) { // There is no way for us to fit the bounds in the display without changing width - // or height. Don't even try it. + // or height. Just move the start to align with the display. + final int layoutDirection = mSupervisor.getConfiguration().getLayoutDirection(); + final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL + ? displayBounds.width() - inOutBounds.width() + : 0; + inOutBounds.offsetTo(left, 0 /* newTop */); return; } diff --git a/services/core/java/com/android/server/wm/TaskPersister.java b/services/core/java/com/android/server/wm/TaskPersister.java index 9705d42464d5..8120dec7e48f 100644 --- a/services/core/java/com/android/server/wm/TaskPersister.java +++ b/services/core/java/com/android/server/wm/TaskPersister.java @@ -83,7 +83,8 @@ public class TaskPersister implements PersisterQueue.Listener { private final ArraySet<Integer> mTmpTaskIds = new ArraySet<>(); TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor, - ActivityTaskManagerService service, RecentTasks recentTasks) { + ActivityTaskManagerService service, RecentTasks recentTasks, + PersisterQueue persisterQueue) { final File legacyImagesDir = new File(systemDir, IMAGES_DIRNAME); if (legacyImagesDir.exists()) { @@ -103,7 +104,7 @@ public class TaskPersister implements PersisterQueue.Listener { mStackSupervisor = stackSupervisor; mService = service; mRecentTasks = recentTasks; - mPersisterQueue = new PersisterQueue(); + mPersisterQueue = persisterQueue; mPersisterQueue.addListener(this); } @@ -117,10 +118,6 @@ public class TaskPersister implements PersisterQueue.Listener { mPersisterQueue.addListener(this); } - void onSystemReady() { - mPersisterQueue.startPersisting(); - } - private void removeThumbnails(TaskRecord task) { mPersisterQueue.removeItems( item -> { @@ -219,21 +216,12 @@ public class TaskPersister implements PersisterQueue.Listener { } void saveImage(Bitmap image, String filePath) { - synchronized (mPersisterQueue) { - final ImageWriteQueueItem item = mPersisterQueue.findLastItem( - queueItem -> queueItem.mFilePath.equals(filePath), ImageWriteQueueItem.class); - if (item != null) { - // replace the Bitmap with the new one. - item.mImage = image; - } else { - mPersisterQueue.addItem(new ImageWriteQueueItem(filePath, image), - /* flush */ false); - } - if (DEBUG) Slog.d(TAG, "saveImage: filePath=" + filePath + " now=" + - SystemClock.uptimeMillis() + " Callers=" + Debug.getCallers(4)); + mPersisterQueue.updateLastOrAddItem(new ImageWriteQueueItem(filePath, image), + /* flush */ false); + if (DEBUG) { + Slog.d(TAG, "saveImage: filePath=" + filePath + " now=" + + SystemClock.uptimeMillis() + " Callers=" + Debug.getCallers(4)); } - - mPersisterQueue.yieldIfQueueTooDeep(); } Bitmap getTaskDescriptionIcon(String filePath) { @@ -603,7 +591,8 @@ public class TaskPersister implements PersisterQueue.Listener { } } - private static class ImageWriteQueueItem implements PersisterQueue.WriteQueueItem { + private static class ImageWriteQueueItem implements + PersisterQueue.WriteQueueItem<ImageWriteQueueItem> { final String mFilePath; Bitmap mImage; @@ -633,6 +622,16 @@ public class TaskPersister implements PersisterQueue.Listener { } @Override + public boolean matches(ImageWriteQueueItem item) { + return mFilePath.equals(item.mFilePath); + } + + @Override + public void updateFrom(ImageWriteQueueItem item) { + mImage = item.mImage; + } + + @Override public String toString() { return "ImageWriteQueueItem{path=" + mFilePath + ", image=(" + mImage.getWidth() + "x" + mImage.getHeight() + ")}"; diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java index 1feb0f150f2f..bd6689fae3ec 100644 --- a/services/core/java/com/android/server/wm/TaskRecord.java +++ b/services/core/java/com/android/server/wm/TaskRecord.java @@ -46,22 +46,7 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_VIA_SDK_VER import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ADD_REMOVE; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_LOCKTASK; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TASKS; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ADD_REMOVE; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_LOCKTASK; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECENTS; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_SHOWN; -import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING; -import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING_TO_TOP; -import static com.android.server.wm.ActivityStackSupervisor.ON_TOP; -import static com.android.server.wm.ActivityStackSupervisor.PAUSE_IMMEDIATELY; -import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS; + import static com.android.server.am.TaskRecordProto.ACTIVITIES; import static com.android.server.am.TaskRecordProto.ACTIVITY_TYPE; import static com.android.server.am.TaskRecordProto.BOUNDS; @@ -75,6 +60,23 @@ import static com.android.server.am.TaskRecordProto.ORIG_ACTIVITY; import static com.android.server.am.TaskRecordProto.REAL_ACTIVITY; import static com.android.server.am.TaskRecordProto.RESIZE_MODE; import static com.android.server.am.TaskRecordProto.STACK_ID; +import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_SHOWN; +import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING; +import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING_TO_TOP; +import static com.android.server.wm.ActivityStackSupervisor.ON_TOP; +import static com.android.server.wm.ActivityStackSupervisor.PAUSE_IMMEDIATELY; +import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ADD_REMOVE; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_LOCKTASK; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TASKS; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ADD_REMOVE; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_LOCKTASK; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECENTS; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; + import static java.lang.Integer.MAX_VALUE; import android.annotation.IntDef; @@ -87,6 +89,7 @@ import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.AppGlobals; import android.app.TaskInfo; +import android.app.WindowConfiguration; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -550,6 +553,8 @@ public class TaskRecord extends ConfigurationContainer implements TaskWindowCont } mWindowContainerController.resize(kept, forced); + saveLaunchingStateIfNeeded(); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); return kept; } finally { @@ -1820,6 +1825,29 @@ public class TaskRecord extends ConfigurationContainer implements TaskWindowCont mService.mStackSupervisor.scheduleUpdateMultiWindowMode(this); } // TODO: Should also take care of Pip mode changes here. + + saveLaunchingStateIfNeeded(); + } + + /** + * Saves launching state if necessary so that we can launch the activity to its latest state. + * It only saves state if this task has been shown to user and it's in fullscreen or freeform + * mode. + */ + void saveLaunchingStateIfNeeded() { + if (!hasBeenVisible) { + // Not ever visible to user. + return; + } + + final int windowingMode = getWindowingMode(); + if (windowingMode != WindowConfiguration.WINDOWING_MODE_FULLSCREEN + && windowingMode != WindowConfiguration.WINDOWING_MODE_FREEFORM) { + return; + } + + // Saves the new state so that we can launch the activity at the same location. + mService.mStackSupervisor.mLaunchParamsPersister.saveTask(this); } /** Clears passed config and fills it with new override values. */ diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java index c35e4d6766a9..26286e2652e0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java @@ -144,6 +144,10 @@ class ActivityTestsBase { return TestActivityDisplay.create(mSupervisor, sNextDisplayId++); } + TestActivityDisplay createNewActivityDisplay(DisplayInfo info) { + return TestActivityDisplay.create(mSupervisor, sNextDisplayId++, info); + } + /** Creates and adds a {@link TestActivityDisplay} to supervisor at the given position. */ TestActivityDisplay addNewActivityDisplayAt(int position) { final TestActivityDisplay display = createNewActivityDisplay(); @@ -586,12 +590,17 @@ class ActivityTestsBase { private final ActivityStackSupervisor mSupervisor; static TestActivityDisplay create(ActivityStackSupervisor supervisor, int displayId) { + return create(supervisor, displayId, new DisplayInfo()); + } + + static TestActivityDisplay create(ActivityStackSupervisor supervisor, int displayId, + DisplayInfo info) { if (displayId == DEFAULT_DISPLAY) { return new TestActivityDisplay(supervisor, supervisor.mDisplayManager.getDisplay(displayId)); } final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId, - new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS); + info, DEFAULT_DISPLAY_ADJUSTMENTS); return new TestActivityDisplay(supervisor, display); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java index 40c20a4b85a3..f8d64e993731 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java @@ -17,6 +17,8 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; @@ -37,8 +39,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityOptions; +import android.content.ComponentName; import android.content.pm.ActivityInfo.WindowLayout; +import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.util.ArrayMap; +import android.util.SparseArray; import androidx.test.filters.MediumTest; @@ -48,6 +54,8 @@ import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; import org.junit.Before; import org.junit.Test; +import java.util.Map; + /** * Tests for exercising {@link LaunchParamsController}. * @@ -58,11 +66,13 @@ import org.junit.Test; @Presubmit public class LaunchParamsControllerTests extends ActivityTestsBase { private LaunchParamsController mController; + private TestLaunchParamsPersister mPersister; @Before public void setUp() throws Exception { mService = createActivityTaskManagerService(); - mController = new LaunchParamsController(mService); + mPersister = new TestLaunchParamsPersister(); + mController = new LaunchParamsController(mService, mPersister); } /** @@ -86,6 +96,31 @@ public class LaunchParamsControllerTests extends ActivityTestsBase { } /** + * Makes sure controller passes stored params to modifiers. + */ + @Test + public void testStoredParamsRecovery() { + final LaunchParamsModifier positioner = mock(LaunchParamsModifier.class); + mController.registerModifier(positioner); + + final ComponentName name = new ComponentName("com.android.foo", ".BarActivity"); + final int userId = 0; + final ActivityRecord activity = new ActivityBuilder(mService).setComponent(name) + .setUid(userId).build(); + final LaunchParams expected = new LaunchParams(); + expected.mPreferredDisplayId = 3; + expected.mWindowingMode = WINDOWING_MODE_PINNED; + expected.mBounds.set(200, 300, 400, 500); + + mPersister.putLaunchParams(userId, name, expected); + + mController.calculate(activity.getTask(), null /*layout*/, activity, null /*source*/, + null /*options*/, new LaunchParams()); + verify(positioner, times(1)).onCalculate(any(), any(), any(), any(), any(), eq(expected), + any()); + } + + /** * Ensures positioners further down the chain are not called when RESULT_DONE is returned. */ @Test @@ -254,6 +289,53 @@ public class LaunchParamsControllerTests extends ActivityTestsBase { assertEquals(windowingMode, afterWindowMode); } + /** + * Ensures that {@link LaunchParamsModifier} requests specifying bounds during + * layout are honored if window is in freeform. + */ + @Test + public void testLayoutTaskBoundsChangeFreeformWindow() { + final Rect expected = new Rect(10, 20, 30, 40); + + final LaunchParams params = new LaunchParams(); + params.mWindowingMode = WINDOWING_MODE_FREEFORM; + params.mBounds.set(expected); + final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params); + final TaskRecord task = new TaskBuilder(mService.mStackSupervisor).build(); + + mController.registerModifier(positioner); + + assertNotEquals(expected, task.getBounds()); + + mController.layoutTask(task, null /* windowLayout */); + + assertEquals(expected, task.getBounds()); + } + + /** + * Ensures that {@link LaunchParamsModifier} requests specifying bounds during + * layout are set to last non-fullscreen bounds. + */ + @Test + public void testLayoutTaskBoundsChangeFixedWindow() { + final Rect expected = new Rect(10, 20, 30, 40); + + final LaunchParams params = new LaunchParams(); + params.mWindowingMode = WINDOWING_MODE_FULLSCREEN; + params.mBounds.set(expected); + final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params); + final TaskRecord task = new TaskBuilder(mService.mStackSupervisor).build(); + + mController.registerModifier(positioner); + + assertNotEquals(expected, task.getBounds()); + + mController.layoutTask(task, null /* windowLayout */); + + assertNotEquals(expected, task.getBounds()); + assertEquals(expected, task.mLastNonFullscreenBounds); + } + public static class InstrumentedPositioner implements LaunchParamsModifier { private final int mReturnVal; @@ -276,4 +358,73 @@ public class LaunchParamsControllerTests extends ActivityTestsBase { return mParams; } } + + /** + * Test double for {@link LaunchParamsPersister}. This class only manages an in-memory storage + * of a mapping from user ID and component name to launch params. + */ + static class TestLaunchParamsPersister extends LaunchParamsPersister { + + private final SparseArray<Map<ComponentName, LaunchParams>> mMap = + new SparseArray<>(); + private final LaunchParams mTmpParams = new LaunchParams(); + + TestLaunchParamsPersister() { + super(null, null, null); + } + + void putLaunchParams(int userId, ComponentName name, LaunchParams params) { + Map<ComponentName, LaunchParams> map = mMap.get(userId); + if (map == null) { + map = new ArrayMap<>(); + mMap.put(userId, map); + } + + LaunchParams paramRecord = map.get(name); + if (paramRecord == null) { + paramRecord = new LaunchParams(); + map.put(name, params); + } + + paramRecord.set(params); + } + + @Override + void onUnlockUser(int userId) { + if (mMap.get(userId) == null) { + mMap.put(userId, new ArrayMap<>()); + } + } + + @Override + void saveTask(TaskRecord task) { + final int userId = task.userId; + final ComponentName realActivity = task.realActivity; + mTmpParams.mPreferredDisplayId = task.getStack().mDisplayId; + mTmpParams.mWindowingMode = task.getWindowingMode(); + if (task.mLastNonFullscreenBounds != null) { + mTmpParams.mBounds.set(task.mLastNonFullscreenBounds); + } else { + mTmpParams.mBounds.setEmpty(); + } + putLaunchParams(userId, realActivity, mTmpParams); + } + + @Override + void getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams params) { + final int userId = task != null ? task.userId : activity.userId; + final ComponentName name = task != null ? task.realActivity : activity.realActivity; + + params.reset(); + final Map<ComponentName, LaunchParams> map = mMap.get(userId); + if (map == null) { + return; + } + + final LaunchParams paramsRecord = map.get(name); + if (paramsRecord != null) { + params.set(paramsRecord); + } + } + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java new file mode 100644 index 000000000000..59e9ce3338ee --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2018 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 android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.INVALID_DISPLAY; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.pm.PackageList; +import android.content.pm.PackageManagerInternal; +import android.graphics.Rect; +import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.FlakyTest; +import androidx.test.filters.MediumTest; + +import com.android.server.LocalServices; +import com.android.server.wm.LaunchParamsController.LaunchParams; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.IntFunction; +import java.util.function.Predicate; + +/** + * Unit tests for {@link LaunchParamsPersister}. + * + * Build/Install/Run: + * atest WmTests:LaunchParamsPersisterTests + */ +@MediumTest +@Presubmit +@FlakyTest(detail = "Confirm stable in post-submit before removing") +public class LaunchParamsPersisterTests extends ActivityTestsBase { + private static final int TEST_USER_ID = 3; + private static final int ALTERNATIVE_USER_ID = 0; + private static final ComponentName TEST_COMPONENT = + ComponentName.createRelative("com.android.foo", ".BarActivity"); + private static final ComponentName ALTERNATIVE_COMPONENT = + ComponentName.createRelative("com.android.foo", ".AlternativeBarActivity"); + + private static final int TEST_WINDOWING_MODE = WINDOWING_MODE_FREEFORM; + private static final Rect TEST_BOUNDS = new Rect(100, 200, 300, 400); + + private static int sNextUniqueId; + + private TestPersisterQueue mPersisterQueue; + private File mFolder; + private ActivityDisplay mTestDisplay; + private String mDisplayUniqueId; + private TaskRecord mTestTask; + private TaskRecord mTaskWithDifferentUser; + private TaskRecord mTaskWithDifferentComponent; + private PackageManagerInternal mMockPmi; + private PackageManagerInternal.PackageListObserver mObserver; + + private final IntFunction<File> mUserFolderGetter = + userId -> new File(mFolder, Integer.toString(userId)); + + private LaunchParamsPersister mTarget; + + private LaunchParams mResult; + + @Before + public void setUp() throws Exception { + mPersisterQueue = new TestPersisterQueue(); + + final File cacheFolder = InstrumentationRegistry.getContext().getCacheDir(); + mFolder = new File(cacheFolder, "launch_params_tests"); + deleteRecursively(mFolder); + + setupActivityTaskManagerService(); + + mDisplayUniqueId = "test:" + Integer.toString(sNextUniqueId++); + final DisplayInfo info = new DisplayInfo(); + info.uniqueId = mDisplayUniqueId; + mTestDisplay = createNewActivityDisplay(info); + mSupervisor.addChild(mTestDisplay, ActivityDisplay.POSITION_TOP); + when(mSupervisor.getActivityDisplay(eq(mDisplayUniqueId))).thenReturn(mTestDisplay); + + ActivityStack stack = mTestDisplay.createStack(TEST_WINDOWING_MODE, + ACTIVITY_TYPE_STANDARD, /* onTop */ true); + mTestTask = new TaskBuilder(mSupervisor).setComponent(TEST_COMPONENT).setStack(stack) + .build(); + mTestTask.userId = TEST_USER_ID; + mTestTask.mLastNonFullscreenBounds = TEST_BOUNDS; + mTestTask.hasBeenVisible = true; + + mTaskWithDifferentComponent = new TaskBuilder(mSupervisor) + .setComponent(ALTERNATIVE_COMPONENT).build(); + mTaskWithDifferentComponent.userId = TEST_USER_ID; + + mTaskWithDifferentUser = new TaskBuilder(mSupervisor).setComponent(TEST_COMPONENT).build(); + mTaskWithDifferentUser.userId = ALTERNATIVE_USER_ID; + + mTarget = new LaunchParamsPersister(mPersisterQueue, mSupervisor, mUserFolderGetter); + + LocalServices.removeServiceForTest(PackageManagerInternal.class); + mMockPmi = mock(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mMockPmi); + when(mMockPmi.getPackageList(any())).thenReturn(new PackageList( + Collections.singletonList(TEST_COMPONENT.getPackageName()), /* observer */ null)); + mTarget.onSystemReady(); + + final ArgumentCaptor<PackageManagerInternal.PackageListObserver> observerCaptor = + ArgumentCaptor.forClass(PackageManagerInternal.PackageListObserver.class); + verify(mMockPmi).getPackageList(observerCaptor.capture()); + mObserver = observerCaptor.getValue(); + + mResult = new LaunchParams(); + mResult.reset(); + } + + @Test + public void testReturnsEmptyLaunchParamsByDefault() { + mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN; + + mTarget.getLaunchParams(mTestTask, null, mResult); + + assertTrue("Default result should be empty.", mResult.isEmpty()); + } + + @Test + public void testSavesAndRestoresLaunchParamsInSameInstance() { + mTarget.saveTask(mTestTask); + + mTarget.getLaunchParams(mTestTask, null, mResult); + + assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId); + assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode); + assertEquals(TEST_BOUNDS, mResult.mBounds); + } + + @Test + public void testFetchesSameResultWithActivity() { + mTarget.saveTask(mTestTask); + + final ActivityRecord activity = new ActivityBuilder(mService).setComponent(TEST_COMPONENT) + .setUid(TEST_USER_ID * UserHandle.PER_USER_RANGE).build(); + + mTarget.getLaunchParams(null, activity, mResult); + + assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId); + assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode); + assertEquals(TEST_BOUNDS, mResult.mBounds); + } + + @Test + public void testReturnsEmptyDisplayIfDisplayIsNotFound() { + mTarget.saveTask(mTestTask); + + when(mSupervisor.getActivityDisplay(eq(mDisplayUniqueId))).thenReturn(null); + + mTarget.getLaunchParams(mTestTask, null, mResult); + + assertEquals(INVALID_DISPLAY, mResult.mPreferredDisplayId); + assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode); + assertEquals(TEST_BOUNDS, mResult.mBounds); + } + + @Test + public void testReturnsEmptyLaunchParamsUserIdMismatch() { + mTarget.saveTask(mTestTask); + + mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN; + mTarget.getLaunchParams(mTaskWithDifferentUser, null, mResult); + + assertTrue("Result should be empty.", mResult.isEmpty()); + } + + @Test + public void testReturnsEmptyLaunchParamsComponentMismatch() { + mTarget.saveTask(mTestTask); + + mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN; + mTarget.getLaunchParams(mTaskWithDifferentComponent, null, mResult); + + assertTrue("Result should be empty.", mResult.isEmpty()); + } + + @Test + public void testSavesAndRestoresLaunchParamsAcrossInstances() { + mTarget.saveTask(mTestTask); + mPersisterQueue.flush(); + + final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor, + mUserFolderGetter); + target.onSystemReady(); + target.onUnlockUser(TEST_USER_ID); + + target.getLaunchParams(mTestTask, null, mResult); + + assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId); + assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode); + assertEquals(TEST_BOUNDS, mResult.mBounds); + } + + @Test + public void testClearsRecordsOfTheUserOnUserCleanUp() { + mTarget.saveTask(mTestTask); + + ActivityStack stack = mTestDisplay.createStack(TEST_WINDOWING_MODE, + ACTIVITY_TYPE_STANDARD, /* onTop */ true); + final TaskRecord anotherTaskOfTheSameUser = new TaskBuilder(mSupervisor) + .setComponent(ALTERNATIVE_COMPONENT) + .setUserId(TEST_USER_ID) + .setStack(stack) + .build(); + anotherTaskOfTheSameUser.setWindowingMode(WINDOWING_MODE_FREEFORM); + anotherTaskOfTheSameUser.setBounds(200, 300, 400, 500); + anotherTaskOfTheSameUser.hasBeenVisible = true; + mTarget.saveTask(anotherTaskOfTheSameUser); + + stack = mTestDisplay.createStack(TEST_WINDOWING_MODE, + ACTIVITY_TYPE_STANDARD, /* onTop */ true); + final TaskRecord anotherTaskOfDifferentUser = new TaskBuilder(mSupervisor) + .setComponent(TEST_COMPONENT) + .setUserId(ALTERNATIVE_USER_ID) + .setStack(stack) + .build(); + anotherTaskOfDifferentUser.setWindowingMode(WINDOWING_MODE_FREEFORM); + anotherTaskOfDifferentUser.setBounds(300, 400, 500, 600); + anotherTaskOfDifferentUser.hasBeenVisible = true; + mTarget.saveTask(anotherTaskOfDifferentUser); + + mTarget.onCleanupUser(TEST_USER_ID); + + mTarget.getLaunchParams(anotherTaskOfDifferentUser, null, mResult); + assertFalse("Shouldn't clear record of a different user.", mResult.isEmpty()); + + mTarget.getLaunchParams(mTestTask, null, mResult); + assertTrue("Should have cleaned record for " + TEST_COMPONENT, mResult.isEmpty()); + + mTarget.getLaunchParams(anotherTaskOfTheSameUser, null, mResult); + assertTrue("Should have cleaned record for " + ALTERNATIVE_COMPONENT, mResult.isEmpty()); + } + + @Test + public void testClearsRecordInMemoryOnPackageUninstalled() { + mTarget.saveTask(mTestTask); + + mObserver.onPackageRemoved(TEST_COMPONENT.getPackageName()); + + mTarget.getLaunchParams(mTestTask, null, mResult); + + assertTrue("Result should be empty.", mResult.isEmpty()); + } + + @Test + public void testClearsWriteQueueItemOnPackageUninstalled() { + mTarget.saveTask(mTestTask); + + mObserver.onPackageRemoved(TEST_COMPONENT.getPackageName()); + + final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor, + mUserFolderGetter); + target.onSystemReady(); + target.onUnlockUser(TEST_USER_ID); + + target.getLaunchParams(mTestTask, null, mResult); + + assertTrue("Result should be empty.", mResult.isEmpty()); + } + + @Test + public void testClearsFileOnPackageUninstalled() { + mTarget.saveTask(mTestTask); + mPersisterQueue.flush(); + + mObserver.onPackageRemoved(TEST_COMPONENT.getPackageName()); + + final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor, + mUserFolderGetter); + target.onSystemReady(); + target.onUnlockUser(TEST_USER_ID); + + target.getLaunchParams(mTestTask, null, mResult); + + assertTrue("Result should be empty.", mResult.isEmpty()); + } + + @Test + public void testClearsRemovedPackageFilesOnStartUp() { + mTarget.saveTask(mTestTask); + mPersisterQueue.flush(); + + when(mMockPmi.getPackageList(any())).thenReturn( + new PackageList(Collections.emptyList(), /* observer */ null)); + + final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor, + mUserFolderGetter); + target.onSystemReady(); + target.onUnlockUser(TEST_USER_ID); + + target.getLaunchParams(mTestTask, null, mResult); + + assertTrue("Result should be empty.", mResult.isEmpty()); + } + + private static boolean deleteRecursively(File file) { + boolean result = true; + if (file.isDirectory()) { + for (File child : file.listFiles()) { + result &= deleteRecursively(child); + } + } + + result &= file.delete(); + return result; + } + + /** + * Test double to {@link PersisterQueue}. This is not thread-safe and caller should always use + * {@link #flush()} to execute write items in it. + */ + static class TestPersisterQueue extends PersisterQueue { + private List<WriteQueueItem> mWriteQueue = new ArrayList<>(); + private List<Listener> mListeners = new ArrayList<>(); + + @Override + void flush() { + while (!mWriteQueue.isEmpty()) { + final WriteQueueItem item = mWriteQueue.remove(0); + final boolean queueEmpty = mWriteQueue.isEmpty(); + for (Listener listener : mListeners) { + listener.onPreProcessItem(queueEmpty); + } + item.process(); + } + } + + @Override + void startPersisting() { + // Do nothing. We're not using threading logic. + } + + @Override + void stopPersisting() { + // Do nothing. We're not using threading logic. + } + + @Override + void addItem(WriteQueueItem item, boolean flush) { + mWriteQueue.add(item); + if (flush) { + flush(); + } + } + + @Override + synchronized <T extends WriteQueueItem> T findLastItem(Predicate<T> predicate, + Class<T> clazz) { + for (int i = mWriteQueue.size() - 1; i >= 0; --i) { + WriteQueueItem writeQueueItem = mWriteQueue.get(i); + if (clazz.isInstance(writeQueueItem)) { + T item = clazz.cast(writeQueueItem); + if (predicate.test(item)) { + return item; + } + } + } + + return null; + } + + @Override + synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate, + Class<T> clazz) { + for (int i = mWriteQueue.size() - 1; i >= 0; --i) { + WriteQueueItem writeQueueItem = mWriteQueue.get(i); + if (clazz.isInstance(writeQueueItem)) { + T item = clazz.cast(writeQueueItem); + if (predicate.test(item)) { + mWriteQueue.remove(i); + } + } + } + } + + @Override + void addListener(Listener listener) { + mListeners.add(listener); + } + + @Override + void yieldIfQueueTooDeep() { + // Do nothing. We're not using threading logic. + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java index 20150b4594cb..434ba932f8ad 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java @@ -185,7 +185,7 @@ public class PersisterQueueTests implements PersisterQueue.Listener { mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS)); assertEquals("Target didn't process all items.", 2, mItemCount.get()); processDuration = SystemClock.uptimeMillis() - dispatchTime; - assertTrue("Target didn't wait enough time before processing item." + assertTrue("Target didn't wait enough time before processing item. Process time: " + processDuration + "ms pre task delay: " + PRE_TASK_DELAY_MS + "ms", processDuration >= PRE_TASK_DELAY_MS); @@ -246,6 +246,39 @@ public class PersisterQueueTests implements PersisterQueue.Listener { } @Test + public void testUpdateLastOrAddItemUpdatesMatchedItem() throws Exception { + mLatch = new CountDownLatch(1); + final MatchingTestItem scheduledItem = new MatchingTestItem(true); + final MatchingTestItem expected = new MatchingTestItem(true); + synchronized (mTarget) { + mTarget.addItem(scheduledItem, false); + mTarget.updateLastOrAddItem(expected, false); + } + + assertSame(expected, scheduledItem.mUpdateFromItem); + assertTrue("Target didn't call callback enough times.", + mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS)); + assertEquals("Target didn't process item.", 1, mItemCount.get()); + } + + @Test + public void testUpdateLastOrAddItemUpdatesAddItemWhenNoMatch() throws Exception { + mLatch = new CountDownLatch(2); + final MatchingTestItem scheduledItem = new MatchingTestItem(false); + final MatchingTestItem expected = new MatchingTestItem(true); + synchronized (mTarget) { + mTarget.addItem(scheduledItem, false); + mTarget.updateLastOrAddItem(expected, false); + } + + assertNull(scheduledItem.mUpdateFromItem); + assertTrue("Target didn't call callback enough times.", + mLatch.await(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS + TIMEOUT_ALLOWANCE, + TimeUnit.MILLISECONDS)); + assertEquals("Target didn't process item.", 2, mItemCount.get()); + } + + @Test public void testRemoveItemsRemoveMatchedItem() throws Exception { mLatch = new CountDownLatch(1); synchronized (mTarget) { @@ -283,18 +316,30 @@ public class PersisterQueueTests implements PersisterQueue.Listener { mSetUpLatch.countDown(); } - private class TestItem implements PersisterQueue.WriteQueueItem { + private class TestItem<T extends TestItem<T>> implements PersisterQueue.WriteQueueItem<T> { @Override public void process() { mItemCount.getAndIncrement(); } } - private class MatchingTestItem extends TestItem { + private class MatchingTestItem extends TestItem<MatchingTestItem> { private boolean mMatching; + private MatchingTestItem mUpdateFromItem; + private MatchingTestItem(boolean matching) { mMatching = matching; } + + @Override + public boolean matches(MatchingTestItem item) { + return item.mMatching; + } + + @Override + public void updateFrom(MatchingTestItem item) { + mUpdateFromItem = item; + } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java index 95965c82ad1f..2168fabb97e5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java @@ -41,6 +41,7 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.Build; import android.platform.test.annotations.Presubmit; +import android.view.Display; import android.view.Gravity; import androidx.test.filters.FlakyTest; @@ -110,6 +111,16 @@ public class TaskLaunchParamsModifierTests extends ActivityTestsBase { } @Test + public void testUsesDefaultDisplayIfPreviousDisplayNotExists() { + mCurrent.mPreferredDisplayId = 19; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(DEFAULT_DISPLAY, mResult.mPreferredDisplayId); + } + + @Test public void testUsesPreviousDisplayIdIfSet() { createNewActivityDisplay(WINDOWING_MODE_FREEFORM); final TestActivityDisplay display = createNewActivityDisplay(WINDOWING_MODE_FULLSCREEN); @@ -856,30 +867,48 @@ public class TaskLaunchParamsModifierTests extends ActivityTestsBase { } @Test - public void testAdjustBoundsToFitDisplay_LargerThanDisplay() { + public void testAdjustBoundsToFitNewDisplay_LargerThanDisplay() { final TestActivityDisplay freeformDisplay = createNewActivityDisplay( WINDOWING_MODE_FREEFORM); - Configuration overrideConfig = new Configuration(); - overrideConfig.setTo(mSupervisor.getOverrideConfiguration()); - overrideConfig.setLayoutDirection(new Locale("ar")); - mSupervisor.onConfigurationChanged(overrideConfig); + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(100, 200, 2120, 1380); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertTrue("Result bounds should start from origin, but it's " + mResult.mBounds, + mResult.mBounds.left == 0 && mResult.mBounds.top == 0); + } + + @Test + public void testAdjustBoundsToFitNewDisplay_LargerThanDisplay_RTL() { + final Configuration overrideConfig = mSupervisor.getOverrideConfiguration(); + // Egyptian Arabic is a RTL language. + overrideConfig.setLayoutDirection(new Locale("ar", "EG")); + mSupervisor.onOverrideConfigurationChanged(overrideConfig); + + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); final ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchDisplayId(freeformDisplay.mDisplayId); - final ActivityRecord source = createSourceActivity(freeformDisplay); - source.setBounds(1720, 680, 1920, 1080); + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(100, 200, 2120, 1380); mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, - mActivity, source, options, mCurrent, mResult)); + mActivity, /* source */ null, options, mCurrent, mResult)); - final Rect displayBounds = freeformDisplay.getBounds(); - assertTrue("display bounds doesn't contain result. display bounds: " - + displayBounds + " result: " + mResult.mBounds, - displayBounds.contains(mResult.mBounds)); + assertTrue("Result bounds should start from origin, but it's " + mResult.mBounds, + mResult.mBounds.left == -100 && mResult.mBounds.top == 0); } @Test @@ -1021,6 +1050,41 @@ public class TaskLaunchParamsModifierTests extends ActivityTestsBase { assertEquals(new Rect(0, 0, 1680, 953), mResult.mBounds); } + @Test + public void testAdjustsBoundsToFitInDisplayFullyResolvedBounds() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = Display.INVALID_DISPLAY; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(-100, -200, 200, 100); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(0, 0, 300, 300), mResult.mBounds); + } + + @Test + public void testAdjustsBoundsToAvoidConflictFullyResolvedBounds() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + addFreeformTaskTo(freeformDisplay, new Rect(0, 0, 200, 100)); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(0, 0, 200, 100); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(120, 0, 320, 100), mResult.mBounds); + } + private TestActivityDisplay createNewActivityDisplay(int windowingMode) { final TestActivityDisplay display = addNewActivityDisplayAt(ActivityDisplay.POSITION_TOP); display.setWindowingMode(windowingMode); |