Implement launch bounds logic in Android (3/3)
This CL introduces persistence to launch bounds logic. It also wires up
the following state changes and persister:
1) freeform resizing;
2) windowing mode change;
3) display change;
4) task closing.
We may still need to persist immersive mode, but that needs further
discussion.
Changed launch bounds modifier a bit so that it won't launch tasks that
are completely out of the new display or conflict to existing tasks.
Bug: 113252871
Test: Manual tests on that freeform launch bounds are persisted across
reboots.
atest WmTests:LaunchParamsPersisterTests
atest WmTests:LaunchParamsControllerTests
atest WmTests:PersisterQueueTests
Change-Id: I20f3056735253c668c7f09c6eb5204e6a5990b1c
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 719a401..4401b50 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -451,6 +451,19 @@
}
/**
+ * 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 1944184..7fcee3db 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 @@
/**
* 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 @@
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 6034f81..a792865 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.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 @@
WindowManagerService mWindowManager;
DisplayManager mDisplayManager;
+ /** Common synchronization logic used to save things to disks. */
+ PersisterQueue mPersisterQueue;
+ LaunchParamsPersister mLaunchParamsPersister;
private LaunchParamsController mLaunchParamsController;
/**
@@ -631,10 +635,16 @@
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;
@@ -4233,6 +4243,25 @@
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 433c05a..1d3b336 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -628,6 +628,7 @@
mAssistUtils = new AssistUtils(mContext);
mVrController.onSystemReady();
mRecentTasks.onSystemReadyLocked();
+ mStackSupervisor.onSystemReady();
}
}
@@ -887,6 +888,20 @@
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 252f47c..0947577 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 @@
*/
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 @@
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 @@
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 @@
task.getStack().setWindowingMode(mTmpParams.mWindowingMode);
}
- if (!mTmpParams.mBounds.isEmpty()) {
- task.updateOverrideConfiguration(mTmpParams.mBounds);
- return true;
- } else {
+ 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;
+ }
+
+ // 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 0000000..72d5143
--- /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 1cfc7ac..a17ee65 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 @@
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 @@
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 c995d3f..15478b4 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 @@
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.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_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 @@
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 @@
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 b7804e8..117984a 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.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 @@
|| 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 @@
// 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 @@
}
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 @@
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 9705d42..8120dec 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 @@
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 @@
mStackSupervisor = stackSupervisor;
mService = service;
mRecentTasks = recentTasks;
- mPersisterQueue = new PersisterQueue();
+ mPersisterQueue = persisterQueue;
mPersisterQueue.addListener(this);
}
@@ -117,10 +118,6 @@
mPersisterQueue.addListener(this);
}
- void onSystemReady() {
- mPersisterQueue.startPersisting();
- }
-
private void removeThumbnails(TaskRecord task) {
mPersisterQueue.removeItems(
item -> {
@@ -219,21 +216,12 @@
}
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 @@
}
}
- private static class ImageWriteQueueItem implements PersisterQueue.WriteQueueItem {
+ private static class ImageWriteQueueItem implements
+ PersisterQueue.WriteQueueItem<ImageWriteQueueItem> {
final String mFilePath;
Bitmap mImage;
@@ -633,6 +622,16 @@
}
@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 d4acb18..dc6324a 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.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.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.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 @@
}
mWindowContainerController.resize(kept, forced);
+ saveLaunchingStateIfNeeded();
+
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
return kept;
} finally {
@@ -1820,6 +1825,29 @@
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 c35e4d6..26286e2 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 @@
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 @@
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 40c20a4..f8d64e9 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.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 org.junit.Before;
import org.junit.Test;
+import java.util.Map;
+
/**
* Tests for exercising {@link LaunchParamsController}.
*
@@ -58,11 +66,13 @@
@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 @@
}
/**
+ * 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 @@
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 @@
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 0000000..59e9ce3
--- /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 20150b4..434ba93 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 @@
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 @@
}
@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 @@
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 95965c8..2168fab 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.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 @@
}
@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 @@
}
@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);
- 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 == 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);
+
+ 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 == -100 && mResult.mBounds.top == 0);
}
@Test
@@ -1021,6 +1050,41 @@
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);