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.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
     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