• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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