1 /* 2 * Copyright 2021 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.launcher3.util; 18 19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM; 21 22 import static java.lang.annotation.RetentionPolicy.SOURCE; 23 24 import android.content.Intent; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.view.View; 28 29 import androidx.annotation.IntDef; 30 31 import com.android.launcher3.logging.StatsLogManager; 32 import com.android.launcher3.model.data.ItemInfo; 33 34 import java.lang.annotation.Retention; 35 36 public final class SplitConfigurationOptions { 37 38 /////////////////////////////////// 39 // Taken from 40 // frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java 41 /** 42 * Stage position isn't specified normally meaning to use what ever it is currently set to. 43 */ 44 public static final int STAGE_POSITION_UNDEFINED = -1; 45 /** 46 * Specifies that a stage is positioned at the top half of the screen if 47 * in portrait mode or at the left half of the screen if in landscape mode. 48 */ 49 public static final int STAGE_POSITION_TOP_OR_LEFT = 0; 50 51 /** 52 * Specifies that a stage is positioned at the bottom half of the screen if 53 * in portrait mode or at the right half of the screen if in landscape mode. 54 */ 55 public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; 56 57 @Retention(SOURCE) 58 @IntDef({STAGE_POSITION_UNDEFINED, STAGE_POSITION_TOP_OR_LEFT, STAGE_POSITION_BOTTOM_OR_RIGHT}) 59 public @interface StagePosition {} 60 61 /** 62 * Stage type isn't specified normally meaning to use what ever the default is. 63 * E.g. exit split-screen and launch the app in fullscreen. 64 */ 65 public static final int STAGE_TYPE_UNDEFINED = -1; 66 /** 67 * The main stage type. 68 */ 69 public static final int STAGE_TYPE_MAIN = 0; 70 71 /** 72 * The side stage type. 73 */ 74 public static final int STAGE_TYPE_SIDE = 1; 75 76 /** 77 * Position independent stage identifier for a given Stage 78 */ 79 public static final int STAGE_TYPE_A = 2; 80 /** 81 * Position independent stage identifier for a given Stage 82 */ 83 public static final int STAGE_TYPE_B = 3; 84 /** 85 * Position independent stage identifier for a given Stage 86 */ 87 public static final int STAGE_TYPE_C = 4; 88 89 @IntDef({ 90 STAGE_TYPE_UNDEFINED, 91 STAGE_TYPE_MAIN, 92 STAGE_TYPE_SIDE, 93 STAGE_TYPE_A, 94 STAGE_TYPE_B, 95 STAGE_TYPE_C 96 }) 97 public @interface StageType {} 98 /////////////////////////////////// 99 100 public static class SplitPositionOption { 101 public final int iconResId; 102 public final int textResId; 103 @StagePosition 104 public final int stagePosition; 105 106 @StageType 107 public final int mStageType; 108 SplitPositionOption(int iconResId, int textResId, int stagePosition, int stageType)109 public SplitPositionOption(int iconResId, int textResId, int stagePosition, int stageType) { 110 this.iconResId = iconResId; 111 this.textResId = textResId; 112 this.stagePosition = stagePosition; 113 mStageType = stageType; 114 } 115 } 116 117 /** 118 * NOTE: Engineers complained about too little ambiguity in the last survey, so there is a class 119 * with the same name/functionality in wm.shell.util (which launcher3 cannot be built against) 120 * 121 * If you make changes here, consider making the same changes there 122 * TODO(b/254378592): We really need to consolidate this 123 */ 124 public static class SplitBounds { 125 public final Rect leftTopBounds; 126 public final Rect rightBottomBounds; 127 /** This rect represents the actual gap between the two apps */ 128 public final Rect visualDividerBounds; 129 // This class is orientation-agnostic, so we compute both for later use 130 private final float topTaskPercent; 131 private final float leftTaskPercent; 132 private final float dividerWidthPercent; 133 private final float dividerHeightPercent; 134 public final int snapPosition; 135 136 /** 137 * If {@code true}, that means at the time of creation of this object, the 138 * split-screened apps were vertically stacked. This is useful in scenarios like 139 * rotation where the bounds won't change, but this variable can indicate what orientation 140 * the bounds were originally in 141 */ 142 public final boolean appsStackedVertically; 143 /** 144 * If {@code true}, that means at the time of creation of this object, the phone was in 145 * seascape orientation. This is important on devices with insets, because they do not split 146 * evenly -- one of the insets must be slightly larger to account for the inset. 147 * From landscape, it is the leftTop task that expands slightly. 148 * From seascape, it is the rightBottom task that expands slightly. 149 */ 150 public final boolean initiatedFromSeascape; 151 public final int leftTopTaskId; 152 public final int rightBottomTaskId; 153 SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId, int rightBottomTaskId, int snapPosition)154 public SplitBounds(Rect leftTopBounds, Rect rightBottomBounds, int leftTopTaskId, 155 int rightBottomTaskId, int snapPosition) { 156 this.leftTopBounds = leftTopBounds; 157 this.rightBottomBounds = rightBottomBounds; 158 this.leftTopTaskId = leftTopTaskId; 159 this.rightBottomTaskId = rightBottomTaskId; 160 this.snapPosition = snapPosition; 161 162 if (rightBottomBounds.top > leftTopBounds.top) { 163 // vertical apps, horizontal divider 164 this.visualDividerBounds = new Rect(leftTopBounds.left, leftTopBounds.bottom, 165 leftTopBounds.right, rightBottomBounds.top); 166 appsStackedVertically = true; 167 initiatedFromSeascape = false; 168 } else { 169 // horizontal apps, vertical divider 170 this.visualDividerBounds = new Rect(leftTopBounds.right, leftTopBounds.top, 171 rightBottomBounds.left, leftTopBounds.bottom); 172 appsStackedVertically = false; 173 // The following check is unreliable on devices without insets 174 // (initiatedFromSeascape will always be set to false.) This happens to be OK for 175 // all our current uses, but should be refactored. 176 // TODO: Create a more reliable check, or refactor how splitting works on devices 177 // with insets. 178 if (rightBottomBounds.width() > leftTopBounds.width()) { 179 initiatedFromSeascape = true; 180 } else { 181 initiatedFromSeascape = false; 182 } 183 } 184 185 float totalWidth = rightBottomBounds.right - leftTopBounds.left; 186 float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; 187 leftTaskPercent = leftTopBounds.width() / totalWidth; 188 topTaskPercent = leftTopBounds.height() / totalHeight; 189 dividerWidthPercent = visualDividerBounds.width() / totalWidth; 190 dividerHeightPercent = visualDividerBounds.height() / totalHeight; 191 } 192 193 /** 194 * Returns the percentage size of the left/top task (compared to the full width/height of 195 * the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and the 196 * right task is 4 units, this method will return 0.4f. 197 */ getLeftTopTaskPercent()198 public float getLeftTopTaskPercent() { 199 // topTaskPercent and leftTaskPercent are defined at creation time, and are not updated 200 // on device rotate, so we have to check appsStackedVertically to return the right 201 // creation-time measurements. 202 return appsStackedVertically ? topTaskPercent : leftTaskPercent; 203 } 204 205 /** 206 * Returns the percentage size of the divider's thickness (compared to the full width/height 207 * of the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and 208 * the right task is 4 units, this method will return 0.2f. 209 */ getDividerPercent()210 public float getDividerPercent() { 211 // dividerHeightPercent and dividerWidthPercent are defined at creation time, and are 212 // not updated on device rotate, so we have to check appsStackedVertically to return 213 // the right creation-time measurements. 214 return appsStackedVertically ? dividerHeightPercent : dividerWidthPercent; 215 } 216 217 /** 218 * Returns the percentage size of the right/bottom task (compared to the full width/height 219 * of the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and 220 * the right task is 4 units, this method will return 0.4f. 221 */ getRightBottomTaskPercent()222 public float getRightBottomTaskPercent() { 223 return 1 - (getLeftTopTaskPercent() + getDividerPercent()); 224 } 225 226 @Override toString()227 public String toString() { 228 return "LeftTop: " + leftTopBounds + ", taskId: " + leftTopTaskId + "\n" 229 + "RightBottom: " + rightBottomBounds + ", taskId: " + rightBottomTaskId + "\n" 230 + "Divider: " + visualDividerBounds + "\n" 231 + "AppsVertical? " + appsStackedVertically + "\n" 232 + "snapPosition: " + snapPosition; 233 } 234 } 235 236 public static class SplitStageInfo { 237 public int taskId = -1; 238 @StagePosition 239 public int stagePosition = STAGE_POSITION_UNDEFINED; 240 @StageType 241 public int stageType = STAGE_TYPE_UNDEFINED; 242 } 243 getLogEventForPosition(@tagePosition int position)244 public static StatsLogManager.EventEnum getLogEventForPosition(@StagePosition int position) { 245 return position == STAGE_POSITION_TOP_OR_LEFT 246 ? LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP 247 : LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM; 248 } 249 getOppositeStagePosition(@tagePosition int position)250 public static @StagePosition int getOppositeStagePosition(@StagePosition int position) { 251 if (position == STAGE_POSITION_UNDEFINED) { 252 return position; 253 } 254 return position == STAGE_POSITION_TOP_OR_LEFT ? STAGE_POSITION_BOTTOM_OR_RIGHT 255 : STAGE_POSITION_TOP_OR_LEFT; 256 } 257 258 public static class SplitSelectSource { 259 260 /** Keep in sync w/ ActivityTaskManager#INVALID_TASK_ID (unreference-able) */ 261 private static final int INVALID_TASK_ID = -1; 262 263 private View view; 264 private Drawable drawable; 265 public final Intent intent; 266 public final SplitPositionOption position; 267 private ItemInfo itemInfo; 268 public final StatsLogManager.EventEnum splitEvent; 269 /** Represents the taskId of the first app to start in split screen */ 270 public int alreadyRunningTaskId = INVALID_TASK_ID; 271 /** 272 * If {@code true}, animates the view represented by {@link #alreadyRunningTaskId} into the 273 * split placeholder view 274 */ 275 public boolean animateCurrentTaskDismissal; 276 SplitSelectSource(View view, Drawable drawable, Intent intent, SplitPositionOption position, ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent)277 public SplitSelectSource(View view, Drawable drawable, Intent intent, 278 SplitPositionOption position, ItemInfo itemInfo, 279 StatsLogManager.EventEnum splitEvent) { 280 this.view = view; 281 this.drawable = drawable; 282 this.intent = intent; 283 this.position = position; 284 this.itemInfo = itemInfo; 285 this.splitEvent = splitEvent; 286 } 287 getDrawable()288 public Drawable getDrawable() { 289 return drawable; 290 } 291 getView()292 public View getView() { 293 return view; 294 } 295 getItemInfo()296 public ItemInfo getItemInfo() { 297 return itemInfo; 298 } 299 } 300 } 301