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.app.ActivityOptions;
23 import android.content.pm.ActivityInfo;
24 import android.graphics.Rect;
25 import android.util.Slog;
26 import android.view.Gravity;
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.server.am.LaunchParamsController.LaunchParams;
29 import com.android.server.am.LaunchParamsController.LaunchParamsModifier;
30 
31 import java.util.ArrayList;
32 
33 /**
34  * Determines where a launching task should be positioned and sized on the display.
35  *
36  * The modifier is fairly simple. For the new task it tries default position based on the gravity
37  * and compares corners of the task with corners of existing tasks. If some two pairs of corners are
38  * sufficiently close enough, it shifts the bounds of the new task and tries again. When it exhausts
39  * all possible shifts, it gives up and puts the task in the original position.
40  *
41  * Note that the only gravities of concern are the corners and the center.
42  */
43 class TaskLaunchParamsModifier implements LaunchParamsModifier {
44     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskLaunchParamsModifier" : TAG_AM;
45 
46     // Determines how close window frames/corners have to be to call them colliding.
47     private static final int BOUNDS_CONFLICT_MIN_DISTANCE = 4;
48 
49     // Task will receive dimensions based on available dimensions divided by this.
50     private static final int WINDOW_SIZE_DENOMINATOR = 2;
51 
52     // Task will receive margins based on available dimensions divided by this.
53     private static final int MARGIN_SIZE_DENOMINATOR = 4;
54 
55     // If task bounds collide with some other, we will step and try again until we find a good
56     // position. The step will be determined by using dimensions and dividing it by this.
57     private static final int STEP_DENOMINATOR = 16;
58 
59     // We always want to step by at least this.
60     private static final int MINIMAL_STEP = 1;
61 
62     // Used to indicate if positioning algorithm is allowed to restart from the beginning, when it
63     // reaches the end of stack bounds.
64     private static final boolean ALLOW_RESTART = true;
65 
66     private static final int SHIFT_POLICY_DIAGONAL_DOWN = 1;
67     private static final int SHIFT_POLICY_HORIZONTAL_RIGHT = 2;
68     private static final int SHIFT_POLICY_HORIZONTAL_LEFT = 3;
69 
70     private final Rect mAvailableRect = new Rect();
71     private final Rect mTmpProposal = new Rect();
72     private final Rect mTmpOriginal = new Rect();
73 
74     /**
75      * Tries to set task's bound in a way that it won't collide with any other task. By colliding
76      * we mean that two tasks have left-top corner very close to each other, so one might get
77      * obfuscated by the other one.
78      */
79     @Override
onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout, ActivityRecord activity, ActivityRecord source, ActivityOptions options, LaunchParams currentParams, LaunchParams outParams)80     public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout,
81                            ActivityRecord activity, ActivityRecord source, ActivityOptions options,
82                            LaunchParams currentParams, LaunchParams outParams) {
83         // We can only apply positioning if we're in a freeform stack.
84         if (task == null || task.getStack() == null || !task.inFreeformWindowingMode()) {
85             return RESULT_SKIP;
86         }
87 
88         final ArrayList<TaskRecord> tasks = task.getStack().getAllTasks();
89 
90         mAvailableRect.set(task.getParent().getBounds());
91 
92         final Rect resultBounds = outParams.mBounds;
93 
94         if (layout == null) {
95             positionCenter(tasks, mAvailableRect, getFreeformWidth(mAvailableRect),
96                     getFreeformHeight(mAvailableRect), resultBounds);
97             return RESULT_CONTINUE;
98         }
99 
100         int width = getFinalWidth(layout, mAvailableRect);
101         int height = getFinalHeight(layout, mAvailableRect);
102         int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
103         int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
104         if (verticalGravity == Gravity.TOP) {
105             if (horizontalGravity == Gravity.RIGHT) {
106                 positionTopRight(tasks, mAvailableRect, width, height, resultBounds);
107             } else {
108                 positionTopLeft(tasks, mAvailableRect, width, height, resultBounds);
109             }
110         } else if (verticalGravity == Gravity.BOTTOM) {
111             if (horizontalGravity == Gravity.RIGHT) {
112                 positionBottomRight(tasks, mAvailableRect, width, height, resultBounds);
113             } else {
114                 positionBottomLeft(tasks, mAvailableRect, width, height, resultBounds);
115             }
116         } else {
117             // Some fancy gravity setting that we don't support yet. We just put the activity in the
118             // center.
119             Slog.w(TAG, "Received unsupported gravity: " + layout.gravity
120                     + ", positioning in the center instead.");
121             positionCenter(tasks, mAvailableRect, width, height, resultBounds);
122         }
123 
124         return RESULT_CONTINUE;
125     }
126 
127     @VisibleForTesting
getFreeformStartLeft(Rect bounds)128     static int getFreeformStartLeft(Rect bounds) {
129         return bounds.left + bounds.width() / MARGIN_SIZE_DENOMINATOR;
130     }
131 
132     @VisibleForTesting
getFreeformStartTop(Rect bounds)133     static int getFreeformStartTop(Rect bounds) {
134         return bounds.top + bounds.height() / MARGIN_SIZE_DENOMINATOR;
135     }
136 
137     @VisibleForTesting
getFreeformWidth(Rect bounds)138     static int getFreeformWidth(Rect bounds) {
139         return bounds.width() / WINDOW_SIZE_DENOMINATOR;
140     }
141 
142     @VisibleForTesting
getFreeformHeight(Rect bounds)143     static int getFreeformHeight(Rect bounds) {
144         return bounds.height() / WINDOW_SIZE_DENOMINATOR;
145     }
146 
147     @VisibleForTesting
getHorizontalStep(Rect bounds)148     static int getHorizontalStep(Rect bounds) {
149         return Math.max(bounds.width() / STEP_DENOMINATOR, MINIMAL_STEP);
150     }
151 
152     @VisibleForTesting
getVerticalStep(Rect bounds)153     static int getVerticalStep(Rect bounds) {
154         return Math.max(bounds.height() / STEP_DENOMINATOR, MINIMAL_STEP);
155     }
156 
157 
158 
getFinalWidth(ActivityInfo.WindowLayout windowLayout, Rect availableRect)159     private int getFinalWidth(ActivityInfo.WindowLayout windowLayout, Rect availableRect) {
160         int width = getFreeformWidth(availableRect);
161         if (windowLayout.width > 0) {
162             width = windowLayout.width;
163         }
164         if (windowLayout.widthFraction > 0) {
165             width = (int) (availableRect.width() * windowLayout.widthFraction);
166         }
167         return width;
168     }
169 
getFinalHeight(ActivityInfo.WindowLayout windowLayout, Rect availableRect)170     private int getFinalHeight(ActivityInfo.WindowLayout windowLayout, Rect availableRect) {
171         int height = getFreeformHeight(availableRect);
172         if (windowLayout.height > 0) {
173             height = windowLayout.height;
174         }
175         if (windowLayout.heightFraction > 0) {
176             height = (int) (availableRect.height() * windowLayout.heightFraction);
177         }
178         return height;
179     }
180 
positionBottomLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width, int height, Rect result)181     private void positionBottomLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
182             int height, Rect result) {
183         mTmpProposal.set(availableRect.left, availableRect.bottom - height,
184                 availableRect.left + width, availableRect.bottom);
185         position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT,
186                 result);
187     }
188 
positionBottomRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width, int height, Rect result)189     private void positionBottomRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
190             int height, Rect result) {
191         mTmpProposal.set(availableRect.right - width, availableRect.bottom - height,
192                 availableRect.right, availableRect.bottom);
193         position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT,
194                 result);
195     }
196 
positionTopLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width, int height, Rect result)197     private void positionTopLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
198             int height, Rect result) {
199         mTmpProposal.set(availableRect.left, availableRect.top,
200                 availableRect.left + width, availableRect.top + height);
201         position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT,
202                 result);
203     }
204 
positionTopRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width, int height, Rect result)205     private void positionTopRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
206             int height, Rect result) {
207         mTmpProposal.set(availableRect.right - width, availableRect.top,
208                 availableRect.right, availableRect.top + height);
209         position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT,
210                 result);
211     }
212 
positionCenter(ArrayList<TaskRecord> tasks, Rect availableRect, int width, int height, Rect result)213     private void positionCenter(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
214             int height, Rect result) {
215         final int defaultFreeformLeft = getFreeformStartLeft(availableRect);
216         final int defaultFreeformTop = getFreeformStartTop(availableRect);
217         mTmpProposal.set(defaultFreeformLeft, defaultFreeformTop,
218                 defaultFreeformLeft + width, defaultFreeformTop + height);
219         position(tasks, availableRect, mTmpProposal, ALLOW_RESTART, SHIFT_POLICY_DIAGONAL_DOWN,
220                 result);
221     }
222 
position(ArrayList<TaskRecord> tasks, Rect availableRect, Rect proposal, boolean allowRestart, int shiftPolicy, Rect result)223     private void position(ArrayList<TaskRecord> tasks, Rect availableRect,
224             Rect proposal, boolean allowRestart, int shiftPolicy, Rect result) {
225         mTmpOriginal.set(proposal);
226         boolean restarted = false;
227         while (boundsConflict(proposal, tasks)) {
228             // Unfortunately there is already a task at that spot, so we need to look for some
229             // other place.
230             shiftStartingPoint(proposal, availableRect, shiftPolicy);
231             if (shiftedTooFar(proposal, availableRect, shiftPolicy)) {
232                 // We don't want the task to go outside of the stack, because it won't look
233                 // nice. Depending on the starting point we either restart, or immediately give up.
234                 if (!allowRestart) {
235                     proposal.set(mTmpOriginal);
236                     break;
237                 }
238                 // We must have started not from the top. Let's restart from there because there
239                 // might be some space there.
240                 proposal.set(availableRect.left, availableRect.top,
241                         availableRect.left + proposal.width(),
242                         availableRect.top + proposal.height());
243                 restarted = true;
244             }
245             if (restarted && (proposal.left > getFreeformStartLeft(availableRect)
246                     || proposal.top > getFreeformStartTop(availableRect))) {
247                 // If we restarted and crossed the initial position, let's not struggle anymore.
248                 // The user already must have ton of tasks visible, we can just smack the new
249                 // one in the center.
250                 proposal.set(mTmpOriginal);
251                 break;
252             }
253         }
254         result.set(proposal);
255     }
256 
shiftedTooFar(Rect start, Rect availableRect, int shiftPolicy)257     private boolean shiftedTooFar(Rect start, Rect availableRect, int shiftPolicy) {
258         switch (shiftPolicy) {
259             case SHIFT_POLICY_HORIZONTAL_LEFT:
260                 return start.left < availableRect.left;
261             case SHIFT_POLICY_HORIZONTAL_RIGHT:
262                 return start.right > availableRect.right;
263             default: // SHIFT_POLICY_DIAGONAL_DOWN
264                 return start.right > availableRect.right || start.bottom > availableRect.bottom;
265         }
266     }
267 
shiftStartingPoint(Rect posposal, Rect availableRect, int shiftPolicy)268     private void shiftStartingPoint(Rect posposal, Rect availableRect, int shiftPolicy) {
269         final int defaultFreeformStepHorizontal = getHorizontalStep(availableRect);
270         final int defaultFreeformStepVertical = getVerticalStep(availableRect);
271 
272         switch (shiftPolicy) {
273             case SHIFT_POLICY_HORIZONTAL_LEFT:
274                 posposal.offset(-defaultFreeformStepHorizontal, 0);
275                 break;
276             case SHIFT_POLICY_HORIZONTAL_RIGHT:
277                 posposal.offset(defaultFreeformStepHorizontal, 0);
278                 break;
279             default: // SHIFT_POLICY_DIAGONAL_DOWN:
280                 posposal.offset(defaultFreeformStepHorizontal, defaultFreeformStepVertical);
281                 break;
282         }
283     }
284 
boundsConflict(Rect proposal, ArrayList<TaskRecord> tasks)285     private static boolean boundsConflict(Rect proposal, ArrayList<TaskRecord> tasks) {
286         for (int i = tasks.size() - 1; i >= 0; i--) {
287             final TaskRecord task = tasks.get(i);
288             if (!task.mActivities.isEmpty() && !task.matchParentBounds()) {
289                 final Rect bounds = task.getOverrideBounds();
290                 if (closeLeftTopCorner(proposal, bounds) || closeRightTopCorner(proposal, bounds)
291                         || closeLeftBottomCorner(proposal, bounds)
292                         || closeRightBottomCorner(proposal, bounds)) {
293                     return true;
294                 }
295             }
296         }
297         return false;
298     }
299 
closeLeftTopCorner(Rect first, Rect second)300     private static final boolean closeLeftTopCorner(Rect first, Rect second) {
301         return Math.abs(first.left - second.left) < BOUNDS_CONFLICT_MIN_DISTANCE
302                 && Math.abs(first.top - second.top) < BOUNDS_CONFLICT_MIN_DISTANCE;
303     }
304 
closeRightTopCorner(Rect first, Rect second)305     private static final boolean closeRightTopCorner(Rect first, Rect second) {
306         return Math.abs(first.right - second.right) < BOUNDS_CONFLICT_MIN_DISTANCE
307                 && Math.abs(first.top - second.top) < BOUNDS_CONFLICT_MIN_DISTANCE;
308     }
309 
closeLeftBottomCorner(Rect first, Rect second)310     private static final boolean closeLeftBottomCorner(Rect first, Rect second) {
311         return Math.abs(first.left - second.left) < BOUNDS_CONFLICT_MIN_DISTANCE
312                 && Math.abs(first.bottom - second.bottom) < BOUNDS_CONFLICT_MIN_DISTANCE;
313     }
314 
closeRightBottomCorner(Rect first, Rect second)315     private static final boolean closeRightBottomCorner(Rect first, Rect second) {
316         return Math.abs(first.right - second.right) < BOUNDS_CONFLICT_MIN_DISTANCE
317                 && Math.abs(first.bottom - second.bottom) < BOUNDS_CONFLICT_MIN_DISTANCE;
318     }
319 }
320