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