1 /* 2 * Copyright (C) 2024 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.wm.shell.shared; 18 19 import static android.app.WindowConfiguration.windowingModeToString; 20 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; 21 import static android.view.Display.INVALID_DISPLAY; 22 23 import android.annotation.IntDef; 24 import android.app.ActivityManager.RecentTaskInfo; 25 import android.app.TaskInfo; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import com.android.wm.shell.shared.split.SplitBounds; 33 34 import kotlin.collections.CollectionsKt; 35 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.List; 39 import java.util.Objects; 40 import java.util.Set; 41 import java.util.stream.Collectors; 42 43 /** 44 * Simple container for recent tasks which should be presented as a single task within the 45 * Overview UI. 46 */ 47 public class GroupedTaskInfo implements Parcelable { 48 49 public static final int TYPE_FULLSCREEN = 1; 50 public static final int TYPE_SPLIT = 2; 51 public static final int TYPE_DESK = 3; 52 public static final int TYPE_MIXED = 4; 53 54 @IntDef(prefix = {"TYPE_"}, value = { 55 TYPE_FULLSCREEN, 56 TYPE_SPLIT, 57 TYPE_DESK, 58 TYPE_MIXED 59 }) 60 public @interface GroupType {} 61 62 /** 63 * The ID of the desk that this `GroupedTaskInfo` represents (when the type is `TYPE_DESK`). The 64 * value is -1 if this is not a desk. 65 */ 66 private final int mDeskId; 67 68 /** 69 * The ID of the display that desk with [mDeskId] is in. 70 */ 71 private final int mDeskDisplayId; 72 73 /** 74 * The type of this particular task info, can be one of TYPE_FULLSCREEN, TYPE_SPLIT or 75 * TYPE_DESK. 76 */ 77 @GroupType 78 protected final int mType; 79 80 /** 81 * The list of tasks associated with this single recent task info. 82 * TYPE_FULLSCREEN: Contains the stack of tasks associated with a single "task" in overview 83 * TYPE_SPLIT: Contains the two split roots of each side 84 * TYPE_DESK: Contains the set of tasks currently in freeform mode contained in desk. 85 */ 86 @Nullable 87 protected final List<TaskInfo> mTasks; 88 89 /** 90 * Only set for TYPE_SPLIT. 91 * 92 * Information about the split bounds. 93 */ 94 @Nullable 95 protected final SplitBounds mSplitBounds; 96 97 /** 98 * Only set for TYPE_DESK. 99 * 100 * TODO(b/348332802): move isMinimized inside each Task object instead once we have a 101 * replacement for RecentTaskInfo 102 */ 103 @Nullable 104 protected final int[] mMinimizedTaskIds; 105 106 /** 107 * Only set for TYPE_MIXED. 108 * 109 * The mixed set of task infos in this group. 110 */ 111 @Nullable 112 protected final List<GroupedTaskInfo> mGroupedTasks; 113 114 /** 115 * Create new for a stack of fullscreen tasks 116 */ forFullscreenTasks(@onNull TaskInfo task)117 public static GroupedTaskInfo forFullscreenTasks(@NonNull TaskInfo task) { 118 return new GroupedTaskInfo(/* deskId = */ -1, /* displayId = */ INVALID_DISPLAY, 119 List.of(task), null, 120 TYPE_FULLSCREEN, /* minimizedFreeformTaskIds = */ null); 121 } 122 123 /** 124 * Create new for a pair of tasks in split screen 125 */ forSplitTasks(@onNull TaskInfo task1, @NonNull TaskInfo task2, @Nullable SplitBounds splitBounds)126 public static GroupedTaskInfo forSplitTasks(@NonNull TaskInfo task1, 127 @NonNull TaskInfo task2, @Nullable SplitBounds splitBounds) { 128 return new GroupedTaskInfo(/* deskId = */ -1, /* displayId = */ INVALID_DISPLAY, 129 List.of(task1, task2), 130 splitBounds, TYPE_SPLIT, /* minimizedFreeformTaskIds = */ null); 131 } 132 133 /** 134 * Create new for a group of freeform tasks that belong to a single desk. 135 */ forDeskTasks( int deskId, int deskDisplayId, @NonNull List<TaskInfo> tasks, @NonNull Set<Integer> minimizedFreeformTaskIds)136 public static GroupedTaskInfo forDeskTasks( 137 int deskId, 138 int deskDisplayId, 139 @NonNull List<TaskInfo> tasks, 140 @NonNull Set<Integer> minimizedFreeformTaskIds) { 141 return new GroupedTaskInfo(deskId, deskDisplayId, tasks, /* splitBounds = */ null, 142 TYPE_DESK, 143 minimizedFreeformTaskIds.stream().mapToInt(i -> i).toArray()); 144 } 145 146 /** 147 * Create new for a group of grouped task infos, those grouped task infos may not be mixed 148 * themselves (ie. multiple depths of mixed grouped task infos are not allowed). 149 */ forMixed(@onNull List<GroupedTaskInfo> groupedTasks)150 public static GroupedTaskInfo forMixed(@NonNull List<GroupedTaskInfo> groupedTasks) { 151 if (groupedTasks.isEmpty()) { 152 throw new IllegalArgumentException("Expected non-empty grouped task list"); 153 } 154 if (groupedTasks.stream().anyMatch(task -> task.mType == TYPE_MIXED)) { 155 throw new IllegalArgumentException("Unexpected grouped task list"); 156 } 157 return new GroupedTaskInfo(groupedTasks); 158 } 159 GroupedTaskInfo( int deskId, int deskDisplayId, @NonNull List<TaskInfo> tasks, @Nullable SplitBounds splitBounds, @GroupType int type, @Nullable int[] minimizedFreeformTaskIds)160 private GroupedTaskInfo( 161 int deskId, 162 int deskDisplayId, 163 @NonNull List<TaskInfo> tasks, 164 @Nullable SplitBounds splitBounds, 165 @GroupType int type, 166 @Nullable int[] minimizedFreeformTaskIds) { 167 mDeskId = deskId; 168 mDeskDisplayId = deskDisplayId; 169 mTasks = tasks; 170 mGroupedTasks = null; 171 mSplitBounds = splitBounds; 172 mType = type; 173 mMinimizedTaskIds = minimizedFreeformTaskIds; 174 ensureAllMinimizedIdsPresent(tasks, minimizedFreeformTaskIds); 175 } 176 GroupedTaskInfo(@onNull List<GroupedTaskInfo> groupedTasks)177 private GroupedTaskInfo(@NonNull List<GroupedTaskInfo> groupedTasks) { 178 mDeskId = -1; 179 mDeskDisplayId = INVALID_DISPLAY; 180 mTasks = null; 181 mGroupedTasks = groupedTasks; 182 mSplitBounds = null; 183 mType = TYPE_MIXED; 184 mMinimizedTaskIds = null; 185 } 186 ensureAllMinimizedIdsPresent( @onNull List<TaskInfo> tasks, @Nullable int[] minimizedFreeformTaskIds)187 private void ensureAllMinimizedIdsPresent( 188 @NonNull List<TaskInfo> tasks, 189 @Nullable int[] minimizedFreeformTaskIds) { 190 if (minimizedFreeformTaskIds == null) { 191 return; 192 } 193 if (!Arrays.stream(minimizedFreeformTaskIds).allMatch( 194 taskId -> tasks.stream().anyMatch(task -> task.taskId == taskId))) { 195 throw new IllegalArgumentException("Minimized task IDs contain non-existent Task ID."); 196 } 197 } 198 GroupedTaskInfo(@onNull Parcel parcel)199 protected GroupedTaskInfo(@NonNull Parcel parcel) { 200 mDeskId = parcel.readInt(); 201 mDeskDisplayId = parcel.readInt(); 202 mTasks = new ArrayList(); 203 final int numTasks = parcel.readInt(); 204 for (int i = 0; i < numTasks; i++) { 205 mTasks.add(new TaskInfo(parcel)); 206 } 207 mGroupedTasks = parcel.createTypedArrayList(GroupedTaskInfo.CREATOR); 208 mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR); 209 mType = parcel.readInt(); 210 mMinimizedTaskIds = parcel.createIntArray(); 211 } 212 213 /** 214 * If TYPE_MIXED, returns the root of the grouped tasks 215 * For all other types, returns this task itself 216 */ 217 @NonNull getBaseGroupedTask()218 public GroupedTaskInfo getBaseGroupedTask() { 219 if (mType == TYPE_MIXED) { 220 return mGroupedTasks.getFirst(); 221 } 222 return this; 223 } 224 225 /** 226 * Get primary {@link TaskInfo}. 227 * 228 * @throws IllegalStateException if the group is TYPE_MIXED. 229 */ 230 @NonNull getTaskInfo1()231 public TaskInfo getTaskInfo1() { 232 if (mType == TYPE_MIXED) { 233 throw new IllegalStateException("No indexed tasks for a mixed task"); 234 } 235 return mTasks.getFirst(); 236 } 237 238 /** 239 * Get secondary {@link TaskInfo}, used primarily for TYPE_SPLIT. 240 * 241 * @throws IllegalStateException if the group is TYPE_MIXED. 242 */ 243 @Nullable getTaskInfo2()244 public TaskInfo getTaskInfo2() { 245 if (mType == TYPE_MIXED) { 246 throw new IllegalStateException("No indexed tasks for a mixed task"); 247 } 248 if (mTasks.size() > 1) { 249 return mTasks.get(1); 250 } 251 return null; 252 } 253 254 /** 255 * @return The task info for the task in this group with the given {@code taskId}. 256 */ 257 @Nullable getTaskById(int taskId)258 public TaskInfo getTaskById(int taskId) { 259 return CollectionsKt.firstOrNull(getTaskInfoList(), taskInfo -> taskInfo.taskId == taskId); 260 } 261 262 /** 263 * Get all {@link RecentTaskInfo}s grouped together. 264 */ 265 @NonNull getTaskInfoList()266 public List<TaskInfo> getTaskInfoList() { 267 if (mType == TYPE_MIXED) { 268 return CollectionsKt.flatMap(mGroupedTasks, groupedTaskInfo -> groupedTaskInfo.mTasks); 269 } else { 270 return mTasks; 271 } 272 } 273 274 /** 275 * @return Whether this grouped task contains a task with the given {@code taskId}. 276 */ containsTask(int taskId)277 public boolean containsTask(int taskId) { 278 return getTaskById(taskId) != null; 279 } 280 281 /** 282 * Returns whether the group is of the given type, if this is a TYPE_MIXED group, then returns 283 * whether the root task info is of the given type. 284 */ isBaseType(@roupType int type)285 public boolean isBaseType(@GroupType int type) { 286 return getBaseGroupedTask().mType == type; 287 } 288 289 /** 290 * Return {@link SplitBounds} if this is a split screen entry or {@code null}. Only valid for 291 * TYPE_SPLIT. 292 */ 293 @Nullable getSplitBounds()294 public SplitBounds getSplitBounds() { 295 if (mType == TYPE_MIXED) { 296 throw new IllegalStateException("No split bounds for a mixed task"); 297 } 298 return mSplitBounds; 299 } 300 301 /** 302 * Returns the ID of the desk represented by `this` if the type is `TYPE_DESK`, or -1 otherwise. 303 */ getDeskId()304 public int getDeskId() { 305 if (mType == TYPE_MIXED) { 306 throw new IllegalStateException("No desk ID for a mixed task"); 307 } 308 return mDeskId; 309 } 310 311 /** 312 * Returns the ID of the display that hosts the desk represented by [mDeskId]. 313 */ getDeskDisplayId()314 public int getDeskDisplayId() { 315 if (mType != TYPE_DESK) { 316 throw new IllegalStateException("No display ID for non desktop task"); 317 } 318 return mDeskDisplayId; 319 } 320 321 /** 322 * Get type of this recents entry. One of {@link GroupType}. 323 * Note: This is deprecated, callers should use `isBaseType()` and not make assumptions about 324 * specific group types 325 */ 326 @Deprecated 327 @GroupType getType()328 public int getType() { 329 return mType; 330 } 331 332 /** 333 * Returns the set of minimized task ids, only valid for TYPE_DESK. 334 */ 335 @Nullable getMinimizedTaskIds()336 public int[] getMinimizedTaskIds() { 337 if (mType == TYPE_MIXED) { 338 throw new IllegalStateException("No minimized task ids for a mixed task"); 339 } 340 return mMinimizedTaskIds; 341 } 342 343 @Override equals(Object obj)344 public boolean equals(Object obj) { 345 if (!(obj instanceof GroupedTaskInfo)) { 346 return false; 347 } 348 GroupedTaskInfo other = (GroupedTaskInfo) obj; 349 return mDeskId == other.mDeskId 350 && mDeskDisplayId == other.mDeskDisplayId 351 && mType == other.mType 352 && Objects.equals(mTasks, other.mTasks) 353 && Objects.equals(mGroupedTasks, other.mGroupedTasks) 354 && Objects.equals(mSplitBounds, other.mSplitBounds) 355 && Arrays.equals(mMinimizedTaskIds, other.mMinimizedTaskIds); 356 } 357 358 @Override hashCode()359 public int hashCode() { 360 return Objects.hash(mDeskId, mDeskDisplayId, mType, mTasks, mGroupedTasks, mSplitBounds, 361 Arrays.hashCode(mMinimizedTaskIds)); 362 } 363 364 @Override toString()365 public String toString() { 366 StringBuilder taskString = new StringBuilder(); 367 if (mType == TYPE_MIXED) { 368 taskString.append("GroupedTasks=" + mGroupedTasks.stream() 369 .map(GroupedTaskInfo::toString) 370 .collect(Collectors.joining(",\n\t", "[\n\t", "\n]"))); 371 } else { 372 taskString.append("Desk ID= ").append(mDeskId).append(", "); 373 taskString.append("Desk Display ID=").append(mDeskDisplayId).append(", "); 374 taskString.append("Tasks=" + mTasks.stream() 375 .map(taskInfo -> getTaskInfoDumpString(taskInfo)) 376 .collect(Collectors.joining(", ", "[", "]"))); 377 if (mSplitBounds != null) { 378 taskString.append(", SplitBounds=").append(mSplitBounds); 379 } 380 taskString.append(", Type=" + typeToString(mType)); 381 taskString.append(", Minimized Task IDs=" + Arrays.toString(mMinimizedTaskIds)); 382 } 383 return taskString.toString(); 384 } 385 getTaskInfoDumpString(TaskInfo taskInfo)386 private String getTaskInfoDumpString(TaskInfo taskInfo) { 387 if (taskInfo == null) { 388 return null; 389 } 390 final boolean isExcluded = (taskInfo.baseIntent.getFlags() 391 & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0; 392 return "id=" + taskInfo.taskId 393 + " winMode=" + windowingModeToString(taskInfo.getWindowingMode()) 394 + " visReq=" + taskInfo.isVisibleRequested 395 + " vis=" + taskInfo.isVisible 396 + " excluded=" + isExcluded 397 + " baseIntent=" 398 + (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null 399 ? taskInfo.baseIntent.getComponent().flattenToShortString() 400 : "null"); 401 } 402 403 @Override writeToParcel(Parcel parcel, int flags)404 public void writeToParcel(Parcel parcel, int flags) { 405 parcel.writeInt(mDeskId); 406 parcel.writeInt(mDeskDisplayId); 407 // We don't use the parcel list methods because we want to only write the TaskInfo state 408 // and not the subclasses (Recents/RunningTaskInfo) whose fields are all deprecated 409 final int tasksSize = mTasks != null ? mTasks.size() : 0; 410 parcel.writeInt(tasksSize); 411 for (int i = 0; i < tasksSize; i++) { 412 mTasks.get(i).writeTaskToParcel(parcel, flags); 413 } 414 parcel.writeTypedList(mGroupedTasks); 415 parcel.writeTypedObject(mSplitBounds, flags); 416 parcel.writeInt(mType); 417 parcel.writeIntArray(mMinimizedTaskIds); 418 } 419 420 @Override describeContents()421 public int describeContents() { 422 return 0; 423 } 424 typeToString(@roupType int type)425 private String typeToString(@GroupType int type) { 426 return switch (type) { 427 case TYPE_FULLSCREEN -> "FULLSCREEN"; 428 case TYPE_SPLIT -> "SPLIT"; 429 case TYPE_DESK -> "DESK"; 430 case TYPE_MIXED -> "MIXED"; 431 default -> "UNKNOWN"; 432 }; 433 } 434 435 public static final Creator<GroupedTaskInfo> CREATOR = new Creator() { 436 @Override 437 public GroupedTaskInfo createFromParcel(Parcel in) { 438 return new GroupedTaskInfo(in); 439 } 440 441 @Override 442 public GroupedTaskInfo[] newArray(int size) { 443 return new GroupedTaskInfo[size]; 444 } 445 }; 446 } 447