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 setDisplay(Display display)81 void setDisplay(Display display) { 82 Point size = new Point(); 83 display.getSize(size); 84 mDisplayWidth = size.x; 85 mDisplayHeight = size.y; 86 } 87 configure(Rect stackBounds)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 */ updateDefaultBounds(TaskRecord task, ArrayList<TaskRecord> tasks, @Nullable ActivityInfo.WindowLayout windowLayout)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 getFinalWidth(ActivityInfo.WindowLayout windowLayout)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 getFinalHeight(ActivityInfo.WindowLayout windowLayout)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 positionBottomLeft(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 positionBottomRight(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 positionTopLeft(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 positionTopRight(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 positionCenter(TaskRecord task, ArrayList<TaskRecord> tasks, int width, int height)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 position(TaskRecord task, ArrayList<TaskRecord> tasks, Rect proposal, boolean allowRestart, int shiftPolicy)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 shiftedToFar(Rect start, int shiftPolicy)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 shiftStartingPoint(Rect posposal, int shiftPolicy)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 boundsConflict(Rect proposal, ArrayList<TaskRecord> tasks)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 closeLeftTopCorner(Rect first, Rect second)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 closeRightTopCorner(Rect first, Rect second)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 closeLeftBottomCorner(Rect first, Rect second)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 closeRightBottomCorner(Rect first, Rect second)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 reset()300 void reset() { 301 mDefaultStartBoundsConfigurationSet = false; 302 } 303 } 304