Initial implementation of LaunchingBoundsController.
This changelist introduces the LaunchingBoundsController, a central
location for calculating launch bounds. It also defines a
positioner interface that can be registered with LaunchingBoundsController
to participate in bounds calculation.
Test: bit FrameworksServicesTests:com.android.server.am.LaunchingTaskPositionerTests
Test: bit FrameworksServicesTests:com.android.server.am.LaunchingActivityPositionerTests
Test: bit FrameworksServicesTests:com.android.server.am.LaunchingBoundsControllerTests
Bug: 64144308
Change-Id: I35eaf095e2edd562375403413050ce82618c44f2
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 941c371..0385497 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -107,7 +107,6 @@
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
-import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Binder;
@@ -346,7 +345,6 @@
private final SparseArray<Rect> mTmpBounds = new SparseArray<>();
private final SparseArray<Rect> mTmpInsetBounds = new SparseArray<>();
private final Rect mTmpRect2 = new Rect();
- private final Point mTmpSize = new Point();
/** Run all ActivityStacks through this */
protected final ActivityStackSupervisor mStackSupervisor;
@@ -5123,24 +5121,14 @@
addTask(task, toTop, "createTaskRecord");
final boolean isLockscreenShown = mService.mStackSupervisor.mKeyguardController
.isKeyguardShowing(mDisplayId != INVALID_DISPLAY ? mDisplayId : DEFAULT_DISPLAY);
- if (!layoutTaskInStack(task, info.windowLayout) && mBounds != null && task.isResizeable()
- && !isLockscreenShown) {
+ if (!mStackSupervisor.getLaunchingBoundsController().layoutTask(task, info.windowLayout)
+ && mBounds != null && task.isResizeable() && !isLockscreenShown) {
task.updateOverrideConfiguration(mBounds);
}
task.createWindowContainer(toTop, (info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0);
return task;
}
- boolean layoutTaskInStack(TaskRecord task, ActivityInfo.WindowLayout windowLayout) {
- if (!task.inFreeformWindowingMode()) {
- return false;
- }
- mStackSupervisor.getLaunchingTaskPositioner()
- .updateDefaultBounds(task, mTaskHistory, windowLayout);
-
- return true;
- }
-
ArrayList<TaskRecord> getAllTasks() {
return new ArrayList<>(mTaskHistory);
}
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index c15b5e2..c5065f1 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -295,7 +295,7 @@
WindowManagerService mWindowManager;
DisplayManager mDisplayManager;
- LaunchingTaskPositioner mTaskPositioner = new LaunchingTaskPositioner();
+ private final LaunchingBoundsController mLaunchingBoundsController;
/** Counter for next free stack ID to use for dynamic activity stacks. */
private int mNextFreeStackId = 0;
@@ -575,6 +575,9 @@
mHandler = new ActivityStackSupervisorHandler(looper);
mActivityMetricsLogger = new ActivityMetricsLogger(this, mService.mContext);
mKeyguardController = new KeyguardController(service, this);
+
+ mLaunchingBoundsController = new LaunchingBoundsController();
+ mLaunchingBoundsController.registerDefaultPositioners(this);
}
void setRecentTasks(RecentTasks recentTasks) {
@@ -2161,8 +2164,8 @@
|| mService.mSupportsFreeformWindowManagement;
}
- LaunchingTaskPositioner getLaunchingTaskPositioner() {
- return mTaskPositioner;
+ LaunchingBoundsController getLaunchingBoundsController() {
+ return mLaunchingBoundsController;
}
protected <T extends ActivityStack> T getStack(int stackId) {
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 6f74d85..4ea51f4 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -151,7 +151,7 @@
private boolean mLaunchTaskBehind;
private int mLaunchFlags;
- private Rect mLaunchBounds;
+ private Rect mLaunchBounds = new Rect();
private ActivityRecord mNotTop;
private boolean mDoResume;
@@ -210,7 +210,7 @@
mLaunchFlags = 0;
mLaunchMode = INVALID_LAUNCH_MODE;
- mLaunchBounds = null;
+ mLaunchBounds.setEmpty();
mNotTop = null;
mDoResume = false;
@@ -1254,7 +1254,10 @@
mPreferredDisplayId = getPreferedDisplayId(mSourceRecord, mStartActivity, options);
- mLaunchBounds = getOverrideBounds(r, options, inTask);
+ mLaunchBounds.setEmpty();
+
+ mSupervisor.getLaunchingBoundsController().calculateBounds(inTask, null /*layout*/, r,
+ options, mLaunchBounds);
mLaunchMode = r.launchMode;
@@ -1725,7 +1728,7 @@
// Target stack got cleared when we all activities were removed above.
// Go ahead and reset it.
mTargetStack = computeStackFocus(mSourceRecord, false /* newTask */,
- null /* bounds */, mLaunchFlags, mOptions);
+ mLaunchFlags, mOptions);
mTargetStack.addTask(task,
!mLaunchTaskBehind /* toTop */, "startActivityUnchecked");
}
@@ -1776,8 +1779,7 @@
private int setTaskFromReuseOrCreateNewTask(
TaskRecord taskToAffiliate, ActivityStack topStack) {
- mTargetStack = computeStackFocus(
- mStartActivity, true, mLaunchBounds, mLaunchFlags, mOptions);
+ mTargetStack = computeStackFocus(mStartActivity, true, mLaunchFlags, mOptions);
// Do no move the target stack to front yet, as we might bail if
// isLockTaskModeViolation fails below.
@@ -1962,7 +1964,7 @@
return START_TASK_TO_FRONT;
}
- if (mLaunchBounds != null) {
+ if (!mLaunchBounds.isEmpty()) {
// TODO: Shouldn't we already know what stack to use by the time we get here?
ActivityStack stack = mSupervisor.getLaunchStack(null, null, mInTask, ON_TOP);
if (stack != mInTask.getStack()) {
@@ -1985,7 +1987,7 @@
}
void updateBounds(TaskRecord task, Rect bounds) {
- if (bounds == null) {
+ if (bounds.isEmpty()) {
return;
}
@@ -1998,8 +2000,7 @@
}
private void setTaskToCurrentTopOrCreateNewTask() {
- mTargetStack = computeStackFocus(mStartActivity, false, null /* bounds */, mLaunchFlags,
- mOptions);
+ mTargetStack = computeStackFocus(mStartActivity, false, mLaunchFlags, mOptions);
if (mDoResume) {
mTargetStack.moveToFront("addingToTopTask");
}
@@ -2062,8 +2063,8 @@
}
}
- private ActivityStack computeStackFocus(ActivityRecord r, boolean newTask, Rect bounds,
- int launchFlags, ActivityOptions aOptions) {
+ private ActivityStack computeStackFocus(ActivityRecord r, boolean newTask, int launchFlags,
+ ActivityOptions aOptions) {
final TaskRecord task = r.getTask();
ActivityStack stack = getLaunchStack(r, launchFlags, task, aOptions);
if (stack != null) {
diff --git a/services/core/java/com/android/server/am/LaunchingActivityPositioner.java b/services/core/java/com/android/server/am/LaunchingActivityPositioner.java
new file mode 100644
index 0000000..5815e98
--- /dev/null
+++ b/services/core/java/com/android/server/am/LaunchingActivityPositioner.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner;
+
+/**
+ * An implementation of {@link LaunchingBoundsPositioner}, which applies the launch bounds specified
+ * inside {@link ActivityOptions#getLaunchBounds()}.
+ */
+public class LaunchingActivityPositioner implements LaunchingBoundsPositioner {
+ private final ActivityStackSupervisor mSupervisor;
+
+ LaunchingActivityPositioner(ActivityStackSupervisor activityStackSupervisor) {
+ mSupervisor = activityStackSupervisor;
+ }
+
+ @Override
+ public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
+ ActivityRecord activity, ActivityOptions options, Rect current, Rect result) {
+ // We only care about figuring out bounds for activities.
+ if (activity == null) {
+ return RESULT_SKIP;
+ }
+
+ // Activity must be resizeable in the specified task.
+ if (!(mSupervisor.canUseActivityOptionsLaunchBounds(options)
+ && (activity.isResizeable() || (task != null && task.isResizeable())))) {
+ return RESULT_SKIP;
+ }
+
+ final Rect bounds = TaskRecord.validateBounds(options.getLaunchBounds());
+
+ // Bounds weren't valid.
+ if (bounds == null) {
+ return RESULT_SKIP;
+ }
+
+ result.set(bounds);
+
+ // When this is the most explicit position specification so we should not allow further
+ // modification of the position.
+ return RESULT_DONE;
+ }
+}
diff --git a/services/core/java/com/android/server/am/LaunchingBoundsController.java b/services/core/java/com/android/server/am/LaunchingBoundsController.java
new file mode 100644
index 0000000..8345ba6
--- /dev/null
+++ b/services/core/java/com/android/server/am/LaunchingBoundsController.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.annotation.IntDef;
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo.WindowLayout;
+import android.graphics.Rect;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_CONTINUE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_DONE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_SKIP;
+
+/**
+ * {@link LaunchingBoundsController} calculates the launch bounds by coordinating between registered
+ * {@link LaunchingBoundsPositioner}.
+ */
+class LaunchingBoundsController {
+ private final List<LaunchingBoundsPositioner> mPositioners = new ArrayList<>();
+
+ // Temporary {@link Rect} for calculations. This is kept separate from {@code mTmpCurrent} and
+ // {@code mTmpResult} to prevent clobbering values.
+ private final Rect mTmpRect = new Rect();
+
+ private final Rect mTmpCurrent = new Rect();
+ private final Rect mTmpResult = new Rect();
+
+ /**
+ * Creates a {@link LaunchingBoundsController} with default registered
+ * {@link LaunchingBoundsPositioner}s.
+ */
+ void registerDefaultPositioners(ActivityStackSupervisor supervisor) {
+ // {@link LaunchingTaskPositioner} handles window layout preferences.
+ registerPositioner(new LaunchingTaskPositioner());
+
+ // {@link LaunchingActivityPositioner} is the most specific positioner and thus should be
+ // registered last (applied first) out of the defaults.
+ registerPositioner(new LaunchingActivityPositioner(supervisor));
+ }
+
+ /**
+ * Returns the position calculated by the registered positioners
+ * @param task The {@link TaskRecord} currently being positioned.
+ * @param layout The specified {@link WindowLayout}.
+ * @param activity The {@link ActivityRecord} currently being positioned.
+ * @param options The {@link ActivityOptions} specified for the activity.
+ * @param result The resulting bounds. If no bounds are set, {@link Rect#isEmpty()} will be
+ * true.
+ */
+ void calculateBounds(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+ ActivityOptions options, Rect result) {
+ result.setEmpty();
+
+ // We start at the last registered {@link LaunchingBoundsPositioner} as this represents
+ // The positioner closest to the product level. Moving back through the list moves closer to
+ // the platform logic.
+ for (int i = mPositioners.size() - 1; i >= 0; --i) {
+ mTmpResult.setEmpty();
+ mTmpCurrent.set(result);
+ final LaunchingBoundsPositioner positioner = mPositioners.get(i);
+
+ switch(positioner.onCalculateBounds(task, layout, activity, options, mTmpCurrent,
+ mTmpResult)) {
+ case RESULT_SKIP:
+ // Do not apply any results when we are told to skip
+ continue;
+ case RESULT_DONE:
+ // Set result and return immediately.
+ result.set(mTmpResult);
+ return;
+ case RESULT_CONTINUE:
+ // Set result and continue
+ result.set(mTmpResult);
+ break;
+ }
+ }
+ }
+
+ /**
+ * A convenience method for laying out a task.
+ * @return {@code true} if bounds were set on the task. {@code false} otherwise.
+ */
+ boolean layoutTask(TaskRecord task, WindowLayout layout) {
+ calculateBounds(task, layout, null /*activity*/, null /*options*/, mTmpRect);
+
+ if (mTmpRect.isEmpty()) {
+ return false;
+ }
+
+ task.updateOverrideConfiguration(mTmpRect);
+
+ return true;
+ }
+
+ /**
+ * Adds a positioner to participate in future bounds calculation. Note that the last registered
+ * {@link LaunchingBoundsPositioner} will be the first to calculate the bounds.
+ */
+ void registerPositioner(LaunchingBoundsPositioner positioner) {
+ if (mPositioners.contains(positioner)) {
+ return;
+ }
+
+ mPositioners.add(positioner);
+ }
+
+ /**
+ * An interface implemented by those wanting to participate in bounds calculation.
+ */
+ interface LaunchingBoundsPositioner {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RESULT_SKIP, RESULT_DONE, RESULT_CONTINUE})
+ @interface Result {}
+
+ // Returned when the positioner does not want to influence the bounds calculation
+ int RESULT_SKIP = 0;
+ // Returned when the positioner has changed the bounds and would like its results to be the
+ // final bounds applied.
+ int RESULT_DONE = 1;
+ // Returned when the positioner has changed the bounds but is okay with other positioners
+ // influencing the bounds.
+ int RESULT_CONTINUE = 2;
+
+ /**
+ * Called when asked to calculate bounds.
+ * @param task The {@link TaskRecord} currently being positioned.
+ * @param layout The specified {@link WindowLayout}.
+ * @param activity The {@link ActivityRecord} currently being positioned.
+ * @param options The {@link ActivityOptions} specified for the activity.
+ * @param current The current bounds. This can differ from the initial bounds as it
+ * represents the modified bounds up to this point.
+ * @param result The {@link Rect} which the positioner should return its modified bounds.
+ * Any merging of the current bounds should be already applied to this
+ * value as well before returning.
+ * @return A {@link Result} representing the result of the bounds calculation.
+ */
+ @Result
+ int onCalculateBounds(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+ ActivityOptions options, Rect current, Rect result);
+ }
+}
diff --git a/services/core/java/com/android/server/am/LaunchingTaskPositioner.java b/services/core/java/com/android/server/am/LaunchingTaskPositioner.java
index 0dc73e9..6389075 100644
--- a/services/core/java/com/android/server/am/LaunchingTaskPositioner.java
+++ b/services/core/java/com/android/server/am/LaunchingTaskPositioner.java
@@ -19,7 +19,7 @@
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import android.annotation.Nullable;
+import android.app.ActivityOptions;
import android.content.pm.ActivityInfo;
import android.graphics.Point;
import android.graphics.Rect;
@@ -36,8 +36,10 @@
* and compares corners of the task with corners of existing tasks. If some two pairs of corners are
* sufficiently close enough, it shifts the bounds of the new task and tries again. When it exhausts
* all possible shifts, it gives up and puts the task in the original position.
+ *
+ * Note that the only gravities of concern are the corners and the center.
*/
-class LaunchingTaskPositioner {
+class LaunchingTaskPositioner implements LaunchingBoundsController.LaunchingBoundsPositioner {
private static final String TAG = TAG_WITH_CLASS_NAME ? "LaunchingTaskPositioner" : TAG_AM;
// Determines how close window frames/corners have to be to call them colliding.
@@ -74,44 +76,50 @@
* Tries to set task's bound in a way that it won't collide with any other task. By colliding
* we mean that two tasks have left-top corner very close to each other, so one might get
* obfuscated by the other one.
- *
- * @param task Task for which we want to find bounds that won't collide with other.
- * @param tasks Existing tasks with which we don't want to collide.
- * @param windowLayout Optional information from the client about how it would like to be sized
- * and positioned.
*/
- void updateDefaultBounds(TaskRecord task, ArrayList<TaskRecord> tasks,
- @Nullable ActivityInfo.WindowLayout windowLayout) {
+ @Override
+ public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
+ ActivityRecord activity, ActivityOptions options, Rect current, Rect result) {
+ // We can only apply positioning if we're in a freeform stack.
+ if (task == null || task.getStack() == null || !task.inFreeformWindowingMode()) {
+ return RESULT_SKIP;
+ }
+
+ final ArrayList<TaskRecord> tasks = task.getStack().getAllTasks();
+
updateAvailableRect(task, mAvailableRect);
- if (windowLayout == null) {
- positionCenter(task, tasks, mAvailableRect, getFreeformWidth(mAvailableRect),
- getFreeformHeight(mAvailableRect));
- return;
+ if (layout == null) {
+ positionCenter(tasks, mAvailableRect, getFreeformWidth(mAvailableRect),
+ getFreeformHeight(mAvailableRect), result);
+ return RESULT_CONTINUE;
}
- int width = getFinalWidth(windowLayout, mAvailableRect);
- int height = getFinalHeight(windowLayout, mAvailableRect);
- int verticalGravity = windowLayout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
- int horizontalGravity = windowLayout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+
+ int width = getFinalWidth(layout, mAvailableRect);
+ int height = getFinalHeight(layout, mAvailableRect);
+ int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
if (verticalGravity == Gravity.TOP) {
if (horizontalGravity == Gravity.RIGHT) {
- positionTopRight(task, tasks, mAvailableRect, width, height);
+ positionTopRight(tasks, mAvailableRect, width, height, result);
} else {
- positionTopLeft(task, tasks, mAvailableRect, width, height);
+ positionTopLeft(tasks, mAvailableRect, width, height, result);
}
} else if (verticalGravity == Gravity.BOTTOM) {
if (horizontalGravity == Gravity.RIGHT) {
- positionBottomRight(task, tasks, mAvailableRect, width, height);
+ positionBottomRight(tasks, mAvailableRect, width, height, result);
} else {
- positionBottomLeft(task, tasks, mAvailableRect, width, height);
+ positionBottomLeft(tasks, mAvailableRect, width, height, result);
}
} else {
// Some fancy gravity setting that we don't support yet. We just put the activity in the
// center.
- Slog.w(TAG, "Received unsupported gravity: " + windowLayout.gravity
+ Slog.w(TAG, "Received unsupported gravity: " + layout.gravity
+ ", positioning in the center instead.");
- positionCenter(task, tasks, mAvailableRect, width, height);
+ positionCenter(tasks, mAvailableRect, width, height, result);
}
+
+ return RESULT_CONTINUE;
}
private void updateAvailableRect(TaskRecord task, Rect availableRect) {
@@ -179,50 +187,50 @@
return height;
}
- private void positionBottomLeft(TaskRecord task, ArrayList<TaskRecord> tasks,
- Rect availableRect, int width, int height) {
+ private void positionBottomLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+ int height, Rect result) {
mTmpProposal.set(availableRect.left, availableRect.bottom - height,
availableRect.left + width, availableRect.bottom);
- position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
- SHIFT_POLICY_HORIZONTAL_RIGHT);
+ position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT,
+ result);
}
- private void positionBottomRight(TaskRecord task, ArrayList<TaskRecord> tasks,
- Rect availableRect, int width, int height) {
+ private void positionBottomRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+ int height, Rect result) {
mTmpProposal.set(availableRect.right - width, availableRect.bottom - height,
availableRect.right, availableRect.bottom);
- position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
- SHIFT_POLICY_HORIZONTAL_LEFT);
+ position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT,
+ result);
}
- private void positionTopLeft(TaskRecord task, ArrayList<TaskRecord> tasks,
- Rect availableRect, int width, int height) {
+ private void positionTopLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+ int height, Rect result) {
mTmpProposal.set(availableRect.left, availableRect.top,
availableRect.left + width, availableRect.top + height);
- position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
- SHIFT_POLICY_HORIZONTAL_RIGHT);
+ position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT,
+ result);
}
- private void positionTopRight(TaskRecord task, ArrayList<TaskRecord> tasks,
- Rect availableRect, int width, int height) {
+ private void positionTopRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+ int height, Rect result) {
mTmpProposal.set(availableRect.right - width, availableRect.top,
availableRect.right, availableRect.top + height);
- position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
- SHIFT_POLICY_HORIZONTAL_LEFT);
+ position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT,
+ result);
}
- private void positionCenter(TaskRecord task, ArrayList<TaskRecord> tasks,
- Rect availableRect, int width, int height) {
+ private void positionCenter(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+ int height, Rect result) {
final int defaultFreeformLeft = getFreeformStartLeft(availableRect);
final int defaultFreeformTop = getFreeformStartTop(availableRect);
mTmpProposal.set(defaultFreeformLeft, defaultFreeformTop,
defaultFreeformLeft + width, defaultFreeformTop + height);
- position(task, tasks, availableRect, mTmpProposal, ALLOW_RESTART,
- SHIFT_POLICY_DIAGONAL_DOWN);
+ position(tasks, availableRect, mTmpProposal, ALLOW_RESTART, SHIFT_POLICY_DIAGONAL_DOWN,
+ result);
}
- private void position(TaskRecord task, ArrayList<TaskRecord> tasks, Rect availableRect,
- Rect proposal, boolean allowRestart, int shiftPolicy) {
+ private void position(ArrayList<TaskRecord> tasks, Rect availableRect,
+ Rect proposal, boolean allowRestart, int shiftPolicy, Rect result) {
mTmpOriginal.set(proposal);
boolean restarted = false;
while (boundsConflict(proposal, tasks)) {
@@ -252,7 +260,7 @@
break;
}
}
- task.updateOverrideConfiguration(proposal);
+ result.set(proposal);
}
private boolean shiftedTooFar(Rect start, Rect availableRect, int shiftPolicy) {
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index c451235..899bf79 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -707,7 +707,7 @@
} else if (toStackWindowingMode == WINDOWING_MODE_FREEFORM) {
Rect bounds = getLaunchBounds();
if (bounds == null) {
- toStack.layoutTaskInStack(this, null);
+ mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(this, null);
bounds = mBounds;
}
kept = resize(bounds, RESIZE_MODE_FORCED, !mightReplaceWindow, deferResume);
@@ -2089,7 +2089,7 @@
if (mLastNonFullscreenBounds != null) {
updateOverrideConfiguration(mLastNonFullscreenBounds);
} else {
- inStack.layoutTaskInStack(this, null);
+ mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(this, null);
}
} else {
updateOverrideConfiguration(inStack.mBounds);
diff --git a/services/tests/servicestests/src/com/android/server/am/LaunchingActivityPositionerTests.java b/services/tests/servicestests/src/com/android/server/am/LaunchingActivityPositionerTests.java
new file mode 100644
index 0000000..0007e8a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/LaunchingActivityPositionerTests.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_DONE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.doAnswer;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_SKIP;
+
+/**
+ * Tests for exercising resizing bounds due to activity options.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.am.LaunchingActivityPositionerTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LaunchingActivityPositionerTests extends ActivityTestsBase {
+ private final ComponentName testActivityComponent =
+ ComponentName.unflattenFromString("com.foo/.BarActivity");
+
+ private LaunchingActivityPositioner mPositioner;
+ private ActivityManagerService mService;
+ private ActivityStack mStack;
+ private TaskRecord mTask;
+ private ActivityRecord mActivity;
+
+ private Rect mCurrent;
+ private Rect mResult;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mService = createActivityManagerService();
+ mPositioner = new LaunchingActivityPositioner(mService.mStackSupervisor);
+ mCurrent = new Rect();
+ mResult = new Rect();
+
+
+ mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ mTask = createTask(mService.mStackSupervisor, testActivityComponent, mStack);
+ mActivity = createActivity(mService, testActivityComponent, mTask);
+ }
+
+
+ @Test
+ public void testSkippedInvocations() throws Exception {
+ // No specified activity should be ignored
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ null /*activity*/, null /*options*/, mCurrent, mResult));
+
+ // No specified activity options should be ignored
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, null /*options*/, mCurrent, mResult));
+
+ // launch bounds specified should be ignored.
+ final ActivityOptions options = ActivityOptions.makeBasic();
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+
+ // Non-resizeable records should be ignored
+ mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+ assertFalse(mActivity.isResizeable());
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+
+ // make record resizeable
+ mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
+ assertTrue(mActivity.isResizeable());
+
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+
+ // Does not support freeform
+ mService.mSupportsFreeformWindowManagement = false;
+ assertFalse(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options));
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+
+ mService.mSupportsFreeformWindowManagement = true;
+ options.setLaunchBounds(new Rect());
+ assertTrue(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options));
+
+ // Invalid bounds
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+ options.setLaunchBounds(new Rect(0, 0, -1, -1));
+ assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+
+ // Valid bounds should cause the positioner to be applied.
+ options.setLaunchBounds(new Rect(0, 0, 100, 100));
+ assertEquals(RESULT_DONE, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+ }
+
+ @Test
+ public void testBoundsExtraction() throws Exception {
+ // Make activity resizeable and enable freeform mode.
+ mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
+ mService.mSupportsFreeformWindowManagement = true;
+
+ ActivityOptions options = ActivityOptions.makeBasic();
+ final Rect proposedBounds = new Rect(20, 30, 45, 40);
+ options.setLaunchBounds(proposedBounds);
+
+ assertEquals(RESULT_DONE, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+ mActivity, options /*options*/, mCurrent, mResult));
+ assertEquals(mResult, proposedBounds);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/LaunchingBoundsControllerTests.java b/services/tests/servicestests/src/com/android/server/am/LaunchingBoundsControllerTests.java
new file mode 100644
index 0000000..f24a273
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/LaunchingBoundsControllerTests.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_DONE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_CONTINUE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_SKIP;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for exercising {@link LaunchingBoundsController}.
+ *
+ * Build/Install/Run:
+ * bit FrameworksServicesTests:com.android.server.am.LaunchingBoundsControllerTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LaunchingBoundsControllerTests extends ActivityTestsBase {
+ private LaunchingBoundsController mController;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mController = new LaunchingBoundsController();
+ }
+
+ /**
+ * Ensures positioners further down the chain are not called when RESULT_DONE is returned.
+ */
+ @Test
+ public void testEarlyExit() {
+ final LaunchingBoundsPositioner ignoredPositioner = mock(LaunchingBoundsPositioner.class);
+ final LaunchingBoundsPositioner earlyExitPositioner =
+ (task, layout, activity, options, current, result) -> RESULT_DONE;
+
+ mController.registerPositioner(ignoredPositioner);
+ mController.registerPositioner(earlyExitPositioner);
+
+ mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*options*/, new Rect());
+ verify(ignoredPositioner, never()).onCalculateBounds(any(), any(), any(), any(), any(),
+ any());
+ }
+
+ /**
+ * Ensures that positioners are called in the correct order.
+ */
+ @Test
+ public void testRegistration() {
+ LaunchingBoundsPositioner earlyExitPositioner =
+ new InstrumentedPositioner(RESULT_DONE, new Rect());
+
+ final LaunchingBoundsPositioner firstPositioner = spy(earlyExitPositioner);
+
+ mController.registerPositioner(firstPositioner);
+
+ mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*options*/, new Rect());
+ verify(firstPositioner, times(1)).onCalculateBounds(any(), any(), any(), any(), any(),
+ any());
+
+ final LaunchingBoundsPositioner secondPositioner = spy(earlyExitPositioner);
+
+ mController.registerPositioner(secondPositioner);
+
+ mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*options*/, new Rect());
+ verify(firstPositioner, times(1)).onCalculateBounds(any(), any(), any(), any(), any(),
+ any());
+ verify(secondPositioner, times(1)).onCalculateBounds(any(), any(), any(), any(), any(),
+ any());
+ }
+
+ /**
+ * Makes sure positioners further down the registration chain are called.
+ */
+ @Test
+ public void testPassThrough() {
+ final LaunchingBoundsPositioner positioner1 = mock(LaunchingBoundsPositioner.class);
+ final InstrumentedPositioner positioner2 = new InstrumentedPositioner(RESULT_CONTINUE,
+ new Rect (0, 0, 30, 20));
+
+ mController.registerPositioner(positioner1);
+ mController.registerPositioner(positioner2);
+
+ mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*options*/, new Rect());
+
+ verify(positioner1, times(1)).onCalculateBounds(any(), any(), any(), any(),
+ eq(positioner2.getLaunchBounds()), any());
+ }
+
+ /**
+ * Ensures skipped results are not propagated.
+ */
+ @Test
+ public void testSkip() {
+ final InstrumentedPositioner positioner1 =
+ new InstrumentedPositioner(RESULT_SKIP, new Rect(0, 0, 10, 10));
+
+
+ final InstrumentedPositioner positioner2 =
+ new InstrumentedPositioner(RESULT_CONTINUE, new Rect(0, 0, 20, 30));
+
+ mController.registerPositioner(positioner1);
+ mController.registerPositioner(positioner2);
+
+ final Rect resultBounds = new Rect();
+
+ mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+ null /*options*/, resultBounds);
+
+ assertEquals(resultBounds, positioner2.getLaunchBounds());
+ }
+
+ public static class InstrumentedPositioner implements LaunchingBoundsPositioner {
+ private int mReturnVal;
+ private Rect mBounds;
+ InstrumentedPositioner(int returnVal, Rect bounds) {
+ mReturnVal = returnVal;
+ mBounds = bounds;
+ }
+
+ @Override
+ public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
+ ActivityRecord activity, ActivityOptions options, Rect current, Rect result) {
+ result.set(mBounds);
+ return mReturnVal;
+ }
+
+ Rect getLaunchBounds() {
+ return mBounds;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/LaunchBoundsTests.java b/services/tests/servicestests/src/com/android/server/am/LaunchingTaskPositionerTests.java
similarity index 81%
rename from services/tests/servicestests/src/com/android/server/am/LaunchBoundsTests.java
rename to services/tests/servicestests/src/com/android/server/am/LaunchingTaskPositionerTests.java
index e6d6831..0d64981 100644
--- a/services/tests/servicestests/src/com/android/server/am/LaunchBoundsTests.java
+++ b/services/tests/servicestests/src/com/android/server/am/LaunchingTaskPositionerTests.java
@@ -17,15 +17,12 @@
package com.android.server.am;
import android.content.ComponentName;
-import android.content.pm.ActivityInfo;
import android.content.pm.ActivityInfo.WindowLayout;
-import android.graphics.Point;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
-import android.view.Display;
import android.view.Gravity;
import org.junit.runner.RunWith;
import org.junit.Before;
@@ -33,10 +30,11 @@
import org.mockito.invocation.InvocationOnMock;
-import java.util.ArrayList;
-
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_CONTINUE;
+
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -48,16 +46,14 @@
* Tests for exercising resizing bounds.
*
* Build/Install/Run:
- * bit FrameworksServicesTests:com.android.server.am.LaunchBoundsTests
+ * bit FrameworksServicesTests:com.android.server.am.LaunchingTaskPositionerTests
*/
@MediumTest
@Presubmit
@RunWith(AndroidJUnit4.class)
-public class LaunchBoundsTests extends ActivityTestsBase {
+public class LaunchingTaskPositionerTests extends ActivityTestsBase {
private final ComponentName testActivityComponent =
ComponentName.unflattenFromString("com.foo/.BarActivity");
- private final ComponentName testActivityComponent2 =
- ComponentName.unflattenFromString("com.foo/.BarActivity2");
private final static int STACK_WIDTH = 100;
private final static int STACK_HEIGHT = 200;
@@ -68,6 +64,11 @@
private ActivityStack mStack;
private TaskRecord mTask;
+ private LaunchingTaskPositioner mPositioner;
+
+ private Rect mCurrent;
+ private Rect mResult;
+
@Before
@Override
public void setUp() throws Exception {
@@ -81,6 +82,11 @@
// We must create the task after resizing to make sure it does not inherit the stack
// dimensions on resize.
mTask = createTask(mService.mStackSupervisor, testActivityComponent, mStack);
+
+ mPositioner = new LaunchingTaskPositioner();
+
+ mResult = new Rect();
+ mCurrent = new Rect();
}
/**
@@ -101,12 +107,9 @@
*/
@Test
public void testLaunchNoWindowLayout() throws Exception {
- final Rect expectedTaskBounds = getDefaultBounds(Gravity.NO_GRAVITY);
-
- mStack.layoutTaskInStack(mTask, null);
-
- // We expect the task to be placed in the middle of the screen with margins applied.
- assertEquals(mTask.mBounds, expectedTaskBounds);
+ assertEquals(RESULT_CONTINUE, mPositioner.onCalculateBounds(mTask, null /*layout*/,
+ null /*record*/, null /*options*/, mCurrent, mResult));
+ assertEquals(getDefaultBounds(Gravity.NO_GRAVITY), mResult);
}
/**
@@ -116,11 +119,10 @@
*/
@Test
public void testlaunchEmptyWindowLayout() throws Exception {
- final Rect expectedTaskBounds = getDefaultBounds(Gravity.NO_GRAVITY);
-
- WindowLayout layout = new WindowLayout(0, 0, 0, 0, 0, 0, 0);
- mStack.layoutTaskInStack(mTask, layout);
- assertEquals(mTask.mBounds, expectedTaskBounds);
+ assertEquals(RESULT_CONTINUE, mPositioner.onCalculateBounds(mTask,
+ new WindowLayout(0, 0, 0, 0, Gravity.NO_GRAVITY, 0, 0), null /*activity*/,
+ null /*options*/, mCurrent, mResult));
+ assertEquals(mResult, getDefaultBounds(Gravity.NO_GRAVITY));
}
/**
@@ -149,9 +151,15 @@
}
private void testGravity(int gravity) {
- final WindowLayout gravityLayout = new WindowLayout(0, 0, 0, 0, gravity, 0, 0);
- mStack.layoutTaskInStack(mTask, gravityLayout);
- assertEquals(mTask.mBounds, getDefaultBounds(gravity));
+ try {
+ assertEquals(RESULT_CONTINUE, mPositioner.onCalculateBounds(mTask,
+ new WindowLayout(0, 0, 0, 0, gravity, 0, 0), null /*activity*/,
+ null /*options*/, mCurrent, mResult));
+ assertEquals(mResult, getDefaultBounds(gravity));
+ } finally {
+ mCurrent.setEmpty();
+ mResult.setEmpty();
+ }
}
/**
@@ -174,7 +182,7 @@
final WindowLayout layout = new WindowLayout(0, 0, 0, 0, gravity, 0, 0);
// layout first task
- mStack.layoutTaskInStack(mTask, layout /*windowLayout*/);
+ mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(mTask, layout);
// Second task will be laid out on top of the first so starting bounds is the same.
final Rect expectedBounds = new Rect(mTask.mBounds);
@@ -192,7 +200,9 @@
mStack);
// layout second task
- mStack.layoutTaskInStack(secondTask, layout /*windowLayout*/);
+ assertEquals(RESULT_CONTINUE,
+ mPositioner.onCalculateBounds(secondTask, layout, null /*activity*/,
+ null /*options*/, mCurrent, mResult));
if ((gravity & (Gravity.TOP | Gravity.RIGHT)) == (Gravity.TOP | Gravity.RIGHT)
|| (gravity & (Gravity.BOTTOM | Gravity.RIGHT))
@@ -207,7 +217,7 @@
LaunchingTaskPositioner.getVerticalStep(mStack.mBounds));
}
- assertEquals(secondTask.mBounds, expectedBounds);
+ assertEquals(mResult, expectedBounds);
} finally {
// Remove task and activity to prevent influencing future tests
if (activity != null) {