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
17package com.android.server.am;
18
19import 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;
24import android.graphics.Point;
25import android.graphics.Rect;
26import android.util.Slog;
27import android.view.Display;
28import android.view.Gravity;
29
30import 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 */
40class 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
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.
112     * @param windowLayout Optional information from the client about how it would like to be sized
113     *                      and positioned.
114     */
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
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
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
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);
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
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
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) {
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
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
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
300    void reset() {
301        mDefaultStartBoundsConfigurationSet = false;
302    }
303}
304