1 /*
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 
17 package com.android.server.am;
18 
19 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
20 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
21 
22 import android.annotation.Nullable;
23 import android.content.pm.ActivityInfo;
24 import android.graphics.Point;
25 import android.graphics.Rect;
26 import android.util.Slog;
27 import android.view.Display;
28 import android.view.Gravity;
29 
30 import java.util.ArrayList;
31 
32 /**
33  * Determines where a launching task should be positioned and sized on the display.
34  *
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.
39  */
40 class LaunchingTaskPositioner {
41     private static final String TAG = TAG_WITH_CLASS_NAME ? "LaunchingTaskPositioner" : TAG_AM;
42 
43     // 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 
59     // 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 
67     private boolean mDefaultStartBoundsConfigurationSet = false;
68     private final Rect mAvailableRect = new Rect();
69     private final Rect mTmpProposal = new Rect();
70     private final Rect mTmpOriginal = new Rect();
71 
72     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 
setDisplay(Display display)81     void setDisplay(Display display) {
82         Point size = new Point();
83         display.getSize(size);
84         mDisplayWidth = size.x;
85         mDisplayHeight = size.y;
86     }
87 
configure(Rect stackBounds)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.
112      * @param windowLayout Optional information from the client about how it would like to be sized
113      *                      and positioned.
114      */
updateDefaultBounds(TaskRecord task, ArrayList<TaskRecord> tasks, @Nullable ActivityInfo.WindowLayout windowLayout)115     void updateDefaultBounds(TaskRecord task, ArrayList<TaskRecord> tasks,
116             @Nullable ActivityInfo.WindowLayout windowLayout) {
117         if (!mDefaultStartBoundsConfigurationSet) {
118             return;
119         }
120         if (windowLayout == null) {
121             positionCenter(task, tasks, mDefaultFreeformWidth, mDefaultFreeformHeight);
122             return;
123         }
124         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;
128         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.
143             Slog.w(TAG, "Received unsupported gravity: " + windowLayout.gravity
144                     + ", positioning in the center instead.");
145             positionCenter(task, tasks, width, height);
146         }
147     }
148 
getFinalWidth(ActivityInfo.WindowLayout windowLayout)149     private int getFinalWidth(ActivityInfo.WindowLayout windowLayout) {
150         int width = mDefaultFreeformWidth;
151         if (windowLayout.width > 0) {
152             width = windowLayout.width;
153         }
154         if (windowLayout.widthFraction > 0) {
155             width = (int) (mAvailableRect.width() * windowLayout.widthFraction);
156         }
157         return width;
158     }
159 
getFinalHeight(ActivityInfo.WindowLayout windowLayout)160     private int getFinalHeight(ActivityInfo.WindowLayout windowLayout) {
161         int height = mDefaultFreeformHeight;
162         if (windowLayout.height > 0) {
163             height = windowLayout.height;
164         }
165         if (windowLayout.heightFraction > 0) {
166             height = (int) (mAvailableRect.height() * windowLayout.heightFraction);
167         }
168         return height;
169     }
170 
positionBottomLeft(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 
positionBottomRight(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 
positionTopLeft(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 
positionTopRight(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 
positionCenter(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 
position(TaskRecord task, ArrayList<TaskRecord> tasks, Rect proposal, boolean allowRestart, int shiftPolicy)206     private void position(TaskRecord task, ArrayList<TaskRecord> tasks, Rect proposal,
207             boolean allowRestart, int shiftPolicy) {
208         mTmpOriginal.set(proposal);
209         boolean restarted = false;
210         while (boundsConflict(proposal, tasks)) {
211             // Unfortunately there is already a task at that spot, so we need to look for some
212             // other place.
213             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());
226                 restarted = true;
227             }
228             if (restarted && (proposal.left > mDefaultFreeformStartX
229                     || proposal.top > mDefaultFreeformStartY)) {
230                 // 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.
233                 proposal.set(mTmpOriginal);
234                 break;
235             }
236         }
237         task.updateOverrideConfiguration(proposal);
238     }
239 
shiftedToFar(Rect start, int shiftPolicy)240     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 
shiftStartingPoint(Rect posposal, int shiftPolicy)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 
boundsConflict(Rect proposal, ArrayList<TaskRecord> tasks)265     private static boolean boundsConflict(Rect proposal, ArrayList<TaskRecord> tasks) {
266         for (int i = tasks.size() - 1; i >= 0; i--) {
267             TaskRecord task = tasks.get(i);
268             if (!task.mActivities.isEmpty() && task.mBounds != null) {
269                 Rect bounds = task.mBounds;
270                 if (closeLeftTopCorner(proposal, bounds) || closeRightTopCorner(proposal, bounds)
271                         || closeLeftBottomCorner(proposal, bounds)
272                         || closeRightBottomCorner(proposal, bounds)) {
273                     return true;
274                 }
275             }
276         }
277         return false;
278     }
279 
closeLeftTopCorner(Rect first, Rect second)280     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 
closeRightTopCorner(Rect first, Rect second)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 
closeLeftBottomCorner(Rect first, Rect second)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 
closeRightBottomCorner(Rect first, Rect second)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 
reset()300     void reset() {
301         mDefaultStartBoundsConfigurationSet = false;
302     }
303 }
304