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.app.ActivityOptions;
23import android.content.pm.ActivityInfo;
24import android.graphics.Rect;
25import android.util.Slog;
26import android.view.Gravity;
27import com.android.internal.annotations.VisibleForTesting;
28import com.android.server.am.LaunchParamsController.LaunchParams;
29import com.android.server.am.LaunchParamsController.LaunchParamsModifier;
30
31import 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 */
43class 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
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
128    static int getFreeformStartLeft(Rect bounds) {
129        return bounds.left + bounds.width() / MARGIN_SIZE_DENOMINATOR;
130    }
131
132    @VisibleForTesting
133    static int getFreeformStartTop(Rect bounds) {
134        return bounds.top + bounds.height() / MARGIN_SIZE_DENOMINATOR;
135    }
136
137    @VisibleForTesting
138    static int getFreeformWidth(Rect bounds) {
139        return bounds.width() / WINDOW_SIZE_DENOMINATOR;
140    }
141
142    @VisibleForTesting
143    static int getFreeformHeight(Rect bounds) {
144        return bounds.height() / WINDOW_SIZE_DENOMINATOR;
145    }
146
147    @VisibleForTesting
148    static int getHorizontalStep(Rect bounds) {
149        return Math.max(bounds.width() / STEP_DENOMINATOR, MINIMAL_STEP);
150    }
151
152    @VisibleForTesting
153    static int getVerticalStep(Rect bounds) {
154        return Math.max(bounds.height() / STEP_DENOMINATOR, MINIMAL_STEP);
155    }
156
157
158
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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