Home | History | Annotate | Download | only in am
      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 
     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