blob: d652341874f154b88e74aadde43f4a0c1c93a1d7 [file] [log] [blame]
Filip Gruszczynskie5390e72015-08-18 16:39:00 -07001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.am;
18
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070019import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
20import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
21
22import android.annotation.Nullable;
23import android.content.pm.ActivityInfo;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070024import android.graphics.Point;
25import android.graphics.Rect;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070026import android.util.Slog;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070027import android.view.Display;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070028import android.view.Gravity;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070029
30import java.util.ArrayList;
31
32/**
33 * Determines where a launching task should be positioned and sized on the display.
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070034 *
35 * The positioner is fairly simple. For the new task it tries default position based on the gravity
36 * and compares corners of the task with corners of existing tasks. If some two pairs of corners are
37 * sufficiently close enough, it shifts the bounds of the new task and tries again. When it exhausts
38 * all possible shifts, it gives up and puts the task in the original position.
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070039 */
40class LaunchingTaskPositioner {
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070041 private static final String TAG = TAG_WITH_CLASS_NAME ? "LaunchingTaskPositioner" : TAG_AM;
42
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070043 // Determines how close window frames/corners have to be to call them colliding.
44 private static final int BOUNDS_CONFLICT_MIN_DISTANCE = 4;
45
46 // Task will receive dimensions based on available dimensions divided by this.
47 private static final int WINDOW_SIZE_DENOMINATOR = 2;
48
49 // Task will receive margins based on available dimensions divided by this.
50 private static final int MARGIN_SIZE_DENOMINATOR = 4;
51
52 // If task bounds collide with some other, we will step and try again until we find a good
53 // position. The step will be determined by using dimensions and dividing it by this.
54 private static final int STEP_DENOMINATOR = 16;
55
56 // We always want to step by at least this.
57 private static final int MINIMAL_STEP = 1;
58
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070059 // Used to indicate if positioning algorithm is allowed to restart from the beginning, when it
60 // reaches the end of stack bounds.
61 private static final boolean ALLOW_RESTART = true;
62
63 private static final int SHIFT_POLICY_DIAGONAL_DOWN = 1;
64 private static final int SHIFT_POLICY_HORIZONTAL_RIGHT = 2;
65 private static final int SHIFT_POLICY_HORIZONTAL_LEFT = 3;
66
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070067 private boolean mDefaultStartBoundsConfigurationSet = false;
68 private final Rect mAvailableRect = new Rect();
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070069 private final Rect mTmpProposal = new Rect();
70 private final Rect mTmpOriginal = new Rect();
71
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070072 private int mDefaultFreeformStartX;
73 private int mDefaultFreeformStartY;
74 private int mDefaultFreeformWidth;
75 private int mDefaultFreeformHeight;
76 private int mDefaultFreeformStepHorizontal;
77 private int mDefaultFreeformStepVertical;
78 private int mDisplayWidth;
79 private int mDisplayHeight;
80
81 void setDisplay(Display display) {
82 Point size = new Point();
83 display.getSize(size);
84 mDisplayWidth = size.x;
85 mDisplayHeight = size.y;
86 }
87
88 void configure(Rect stackBounds) {
89 if (stackBounds == null) {
90 mAvailableRect.set(0, 0, mDisplayWidth, mDisplayHeight);
91 } else {
92 mAvailableRect.set(stackBounds);
93 }
94 int width = mAvailableRect.width();
95 int height = mAvailableRect.height();
96 mDefaultFreeformStartX = mAvailableRect.left + width / MARGIN_SIZE_DENOMINATOR;
97 mDefaultFreeformStartY = mAvailableRect.top + height / MARGIN_SIZE_DENOMINATOR;
98 mDefaultFreeformWidth = width / WINDOW_SIZE_DENOMINATOR;
99 mDefaultFreeformHeight = height / WINDOW_SIZE_DENOMINATOR;
100 mDefaultFreeformStepHorizontal = Math.max(width / STEP_DENOMINATOR, MINIMAL_STEP);
101 mDefaultFreeformStepVertical = Math.max(height / STEP_DENOMINATOR, MINIMAL_STEP);
102 mDefaultStartBoundsConfigurationSet = true;
103 }
104
105 /**
106 * Tries to set task's bound in a way that it won't collide with any other task. By colliding
107 * we mean that two tasks have left-top corner very close to each other, so one might get
108 * obfuscated by the other one.
109 *
110 * @param task Task for which we want to find bounds that won't collide with other.
111 * @param tasks Existing tasks with which we don't want to collide.
Andrii Kulian2e751b82016-03-16 16:59:32 -0700112 * @param windowLayout Optional information from the client about how it would like to be sized
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700113 * and positioned.
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700114 */
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700115 void updateDefaultBounds(TaskRecord task, ArrayList<TaskRecord> tasks,
Andrii Kulian2e751b82016-03-16 16:59:32 -0700116 @Nullable ActivityInfo.WindowLayout windowLayout) {
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700117 if (!mDefaultStartBoundsConfigurationSet) {
118 return;
119 }
Andrii Kulian2e751b82016-03-16 16:59:32 -0700120 if (windowLayout == null) {
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700121 positionCenter(task, tasks, mDefaultFreeformWidth, mDefaultFreeformHeight);
122 return;
123 }
Andrii Kulian2e751b82016-03-16 16:59:32 -0700124 int width = getFinalWidth(windowLayout);
125 int height = getFinalHeight(windowLayout);
126 int verticalGravity = windowLayout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
127 int horizontalGravity = windowLayout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700128 if (verticalGravity == Gravity.TOP) {
129 if (horizontalGravity == Gravity.RIGHT) {
130 positionTopRight(task, tasks, width, height);
131 } else {
132 positionTopLeft(task, tasks, width, height);
133 }
134 } else if (verticalGravity == Gravity.BOTTOM) {
135 if (horizontalGravity == Gravity.RIGHT) {
136 positionBottomRight(task, tasks, width, height);
137 } else {
138 positionBottomLeft(task, tasks, width, height);
139 }
140 } else {
141 // Some fancy gravity setting that we don't support yet. We just put the activity in the
142 // center.
Andrii Kulian2e751b82016-03-16 16:59:32 -0700143 Slog.w(TAG, "Received unsupported gravity: " + windowLayout.gravity
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700144 + ", positioning in the center instead.");
145 positionCenter(task, tasks, width, height);
146 }
147 }
148
Andrii Kulian2e751b82016-03-16 16:59:32 -0700149 private int getFinalWidth(ActivityInfo.WindowLayout windowLayout) {
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700150 int width = mDefaultFreeformWidth;
Andrii Kulian2e751b82016-03-16 16:59:32 -0700151 if (windowLayout.width > 0) {
152 width = windowLayout.width;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700153 }
Andrii Kulian2e751b82016-03-16 16:59:32 -0700154 if (windowLayout.widthFraction > 0) {
155 width = (int) (mAvailableRect.width() * windowLayout.widthFraction);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700156 }
157 return width;
158 }
159
Andrii Kulian2e751b82016-03-16 16:59:32 -0700160 private int getFinalHeight(ActivityInfo.WindowLayout windowLayout) {
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700161 int height = mDefaultFreeformHeight;
Andrii Kulian2e751b82016-03-16 16:59:32 -0700162 if (windowLayout.height > 0) {
163 height = windowLayout.height;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700164 }
Andrii Kulian2e751b82016-03-16 16:59:32 -0700165 if (windowLayout.heightFraction > 0) {
166 height = (int) (mAvailableRect.height() * windowLayout.heightFraction);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700167 }
168 return height;
169 }
170
171 private void positionBottomLeft(TaskRecord task, ArrayList<TaskRecord> tasks, int width,
172 int height) {
173 mTmpProposal.set(mAvailableRect.left, mAvailableRect.bottom - height,
174 mAvailableRect.left + width, mAvailableRect.bottom);
175 position(task, tasks, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT);
176 }
177
178 private void positionBottomRight(TaskRecord task, ArrayList<TaskRecord> tasks, int width,
179 int height) {
180 mTmpProposal.set(mAvailableRect.right - width, mAvailableRect.bottom - height,
181 mAvailableRect.right, mAvailableRect.bottom);
182 position(task, tasks, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT);
183 }
184
185 private void positionTopLeft(TaskRecord task, ArrayList<TaskRecord> tasks, int width,
186 int height) {
187 mTmpProposal.set(mAvailableRect.left, mAvailableRect.top,
188 mAvailableRect.left + width, mAvailableRect.top + height);
189 position(task, tasks, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT);
190 }
191
192 private void positionTopRight(TaskRecord task, ArrayList<TaskRecord> tasks, int width,
193 int height) {
194 mTmpProposal.set(mAvailableRect.right - width, mAvailableRect.top,
195 mAvailableRect.right, mAvailableRect.top + height);
196 position(task, tasks, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT);
197 }
198
199 private void positionCenter(TaskRecord task, ArrayList<TaskRecord> tasks, int width,
200 int height) {
201 mTmpProposal.set(mDefaultFreeformStartX, mDefaultFreeformStartY,
202 mDefaultFreeformStartX + width, mDefaultFreeformStartY + height);
203 position(task, tasks, mTmpProposal, ALLOW_RESTART, SHIFT_POLICY_DIAGONAL_DOWN);
204 }
205
206 private void position(TaskRecord task, ArrayList<TaskRecord> tasks, Rect proposal,
207 boolean allowRestart, int shiftPolicy) {
208 mTmpOriginal.set(proposal);
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700209 boolean restarted = false;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700210 while (boundsConflict(proposal, tasks)) {
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700211 // Unfortunately there is already a task at that spot, so we need to look for some
212 // other place.
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700213 shiftStartingPoint(proposal, shiftPolicy);
214 if (shiftedToFar(proposal, shiftPolicy)) {
215 // We don't want the task to go outside of the stack, because it won't look
216 // nice. Depending on the starting point we either restart, or immediately give up.
217 if (!allowRestart) {
218 proposal.set(mTmpOriginal);
219 break;
220 }
221 // We must have started not from the top. Let's restart from there because there
222 // might be some space there.
223 proposal.set(mAvailableRect.left, mAvailableRect.top,
224 mAvailableRect.left + proposal.width(),
225 mAvailableRect.top + proposal.height());
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700226 restarted = true;
227 }
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700228 if (restarted && (proposal.left > mDefaultFreeformStartX
229 || proposal.top > mDefaultFreeformStartY)) {
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700230 // If we restarted and crossed the initial position, let's not struggle anymore.
231 // The user already must have ton of tasks visible, we can just smack the new
232 // one in the center.
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700233 proposal.set(mTmpOriginal);
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700234 break;
235 }
236 }
Filip Gruszczynskiaff7f132015-09-02 17:21:21 -0700237 task.updateOverrideConfiguration(proposal);
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700238 }
239
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700240 private boolean shiftedToFar(Rect start, int shiftPolicy) {
241 switch (shiftPolicy) {
242 case SHIFT_POLICY_HORIZONTAL_LEFT:
243 return start.left < mAvailableRect.left;
244 case SHIFT_POLICY_HORIZONTAL_RIGHT:
245 return start.right > mAvailableRect.right;
246 default: // SHIFT_POLICY_DIAGONAL_DOWN
247 return start.right > mAvailableRect.right || start.bottom > mAvailableRect.bottom;
248 }
249 }
250
251 private void shiftStartingPoint(Rect posposal, int shiftPolicy) {
252 switch (shiftPolicy) {
253 case SHIFT_POLICY_HORIZONTAL_LEFT:
254 posposal.offset(-mDefaultFreeformStepHorizontal, 0);
255 break;
256 case SHIFT_POLICY_HORIZONTAL_RIGHT:
257 posposal.offset(mDefaultFreeformStepHorizontal, 0);
258 break;
259 default: // SHIFT_POLICY_DIAGONAL_DOWN:
260 posposal.offset(mDefaultFreeformStepHorizontal, mDefaultFreeformStepVertical);
261 break;
262 }
263 }
264
265 private static boolean boundsConflict(Rect proposal, ArrayList<TaskRecord> tasks) {
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700266 for (int i = tasks.size() - 1; i >= 0; i--) {
267 TaskRecord task = tasks.get(i);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700268 if (!task.mActivities.isEmpty() && task.mBounds != null) {
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700269 Rect bounds = task.mBounds;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700270 if (closeLeftTopCorner(proposal, bounds) || closeRightTopCorner(proposal, bounds)
271 || closeLeftBottomCorner(proposal, bounds)
272 || closeRightBottomCorner(proposal, bounds)) {
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700273 return true;
274 }
275 }
276 }
277 return false;
278 }
279
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700280 private static final boolean closeLeftTopCorner(Rect first, Rect second) {
281 return Math.abs(first.left - second.left) < BOUNDS_CONFLICT_MIN_DISTANCE
282 && Math.abs(first.top - second.top) < BOUNDS_CONFLICT_MIN_DISTANCE;
283 }
284
285 private static final boolean closeRightTopCorner(Rect first, Rect second) {
286 return Math.abs(first.right - second.right) < BOUNDS_CONFLICT_MIN_DISTANCE
287 && Math.abs(first.top - second.top) < BOUNDS_CONFLICT_MIN_DISTANCE;
288 }
289
290 private static final boolean closeLeftBottomCorner(Rect first, Rect second) {
291 return Math.abs(first.left - second.left) < BOUNDS_CONFLICT_MIN_DISTANCE
292 && Math.abs(first.bottom - second.bottom) < BOUNDS_CONFLICT_MIN_DISTANCE;
293 }
294
295 private static final boolean closeRightBottomCorner(Rect first, Rect second) {
296 return Math.abs(first.right - second.right) < BOUNDS_CONFLICT_MIN_DISTANCE
297 && Math.abs(first.bottom - second.bottom) < BOUNDS_CONFLICT_MIN_DISTANCE;
298 }
299
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700300 void reset() {
301 mDefaultStartBoundsConfigurationSet = false;
302 }
303}