1 /*
2  * Copyright (C) 2023 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.car.carlauncher.recents;
18 
19 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
20 
21 import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
22 import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SINGLE;
23 import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SPLIT;
24 
25 import static com.google.common.truth.Truth.assertThat;
26 
27 import static org.mockito.ArgumentMatchers.any;
28 import static org.mockito.ArgumentMatchers.anyBoolean;
29 import static org.mockito.ArgumentMatchers.anyInt;
30 import static org.mockito.ArgumentMatchers.eq;
31 import static org.mockito.ArgumentMatchers.nullable;
32 import static org.mockito.Mockito.mock;
33 import static org.mockito.Mockito.never;
34 import static org.mockito.Mockito.verify;
35 import static org.mockito.Mockito.when;
36 
37 import android.app.Activity;
38 import android.app.ActivityManager;
39 import android.app.ActivityOptions;
40 import android.content.ComponentName;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.pm.ActivityInfo;
44 import android.content.pm.ApplicationInfo;
45 import android.content.pm.PackageManager;
46 import android.graphics.Bitmap;
47 import android.graphics.drawable.BitmapDrawable;
48 import android.graphics.drawable.Drawable;
49 import android.os.Handler;
50 import android.os.RemoteException;
51 import android.testing.TestableContext;
52 
53 import androidx.test.platform.app.InstrumentationRegistry;
54 
55 import com.android.systemui.shared.recents.model.Task;
56 import com.android.systemui.shared.system.ActivityManagerWrapper;
57 import com.android.systemui.shared.system.PackageManagerWrapper;
58 import com.android.wm.shell.recents.IRecentTasks;
59 import com.android.wm.shell.util.GroupedRecentTaskInfo;
60 
61 import com.google.common.util.concurrent.MoreExecutors;
62 
63 import org.junit.After;
64 import org.junit.Before;
65 import org.junit.Rule;
66 import org.junit.Test;
67 import org.mockito.Mock;
68 import org.mockito.MockitoAnnotations;
69 import org.mockito.stubbing.Answer;
70 
71 import java.util.ArrayList;
72 import java.util.List;
73 
74 
75 public class RecentTasksProviderTest {
76     private static final int RECENT_TASKS_LENGTH = 20;
77     private static final int SPLIT_RECENT_TASKS_LENGTH = 2;
78     private static final int FREEFORM_RECENT_TASKS_LENGTH = 3;
79 
80     private RecentTasksProvider mRecentTasksProvider;
81     private GroupedRecentTaskInfo[] mGroupedRecentTaskInfo;
82 
83     @Mock
84     private IRecentTasks mRecentTaskProxy;
85     @Mock
86     private ActivityManagerWrapper mActivityManagerWrapper;
87     @Mock
88     private PackageManagerWrapper mPackageManagerWrapper;
89     @Mock
90     private RecentTasksProviderInterface.RecentsDataChangeListener mRecentsDataChangeListenerMock;
91     @Mock
92     private Task mTask;
93     @Mock
94     private Task.TaskKey mTaskKey;
95     @Mock
96     private ActivityManager.TaskDescription mTaskDescription;
97     @Mock
98     private Bitmap mIconBitmap;
99     @Mock
100     private Drawable mIconDrawable;
101     @Mock
102     private Drawable mDefaultIconDrawable;
103     @Mock
104     private ComponentName mComponent;
105     @Mock
106     private ActivityInfo mActivityInfo;
107     @Mock
108     private Intent mBaseIntent;
109     @Mock
110     private Handler mHandler;
111 
112     @Rule
113     public final TestableContext mContext = new TestableContext(
114             InstrumentationRegistry.getInstrumentation().getTargetContext()) {
115         @Override
116         public Context createApplicationContext(ApplicationInfo application, int flags) {
117             return this;
118         }
119     };
120 
121     @Before
setup()122     public void setup() throws RemoteException {
123         MockitoAnnotations.initMocks(this);
124         initRecentTaskList();
125         when(mRecentTaskProxy.getRecentTasks(anyInt(), eq(RECENT_IGNORE_UNAVAILABLE),
126                 anyInt())).thenReturn(mGroupedRecentTaskInfo);
127         RecentTasksProvider.setExecutor(MoreExecutors.directExecutor());
128         when(mHandler.post(any(Runnable.class))).thenAnswer((Answer<Runnable>) invocation -> {
129             ((Runnable) invocation.getArgument(0)).run();
130             return null;
131         });
132         RecentTasksProvider.setHandler(mHandler);
133         mRecentTasksProvider = RecentTasksProvider.getInstance();
134         mRecentTasksProvider.setActivityManagerWrapper(mActivityManagerWrapper);
135         mRecentTasksProvider.setPackageManagerWrapper(mPackageManagerWrapper);
136         mRecentTasksProvider.init(mContext, mRecentTaskProxy);
137     }
138 
139     @After
cleanup()140     public void cleanup() {
141         mRecentTasksProvider.mRecentTaskIdToTaskMap.clear();
142     }
143 
144     @Test
getRecentTasksAsync_triggers_recentTasksFetched()145     public void getRecentTasksAsync_triggers_recentTasksFetched() throws InterruptedException {
146         mRecentTasksProvider.setRecentsDataChangeListener(mRecentsDataChangeListenerMock);
147 
148         mRecentTasksProvider.getRecentTasksAsync();
149 
150         verify(mRecentsDataChangeListenerMock).recentTasksFetched();
151     }
152 
153     @Test
getRecentTasksAsync_trigger_recentTaskThumbnailChange_forAllTasks()154     public void getRecentTasksAsync_trigger_recentTaskThumbnailChange_forAllTasks() {
155         mRecentTasksProvider.setRecentsDataChangeListener(mRecentsDataChangeListenerMock);
156 
157         mRecentTasksProvider.getRecentTasksAsync();
158 
159         for (int i = 0; i < RECENT_TASKS_LENGTH; i++) {
160             verify(mRecentsDataChangeListenerMock).recentTaskThumbnailChange(i);
161         }
162     }
163 
164     @Test
getRecentTasksAsync_trigger_recentTaskIconChange_forAllTasks()165     public void getRecentTasksAsync_trigger_recentTaskIconChange_forAllTasks() {
166         mRecentTasksProvider.setRecentsDataChangeListener(mRecentsDataChangeListenerMock);
167 
168         mRecentTasksProvider.getRecentTasksAsync();
169 
170         for (int i = 0; i < RECENT_TASKS_LENGTH; i++) {
171             verify(mRecentsDataChangeListenerMock).recentTaskIconChange(i);
172         }
173     }
174 
175     @Test
getRecentTasksAsync_getRecentTaskIds_returnsAllInOrder_fetchedTaskIds()176     public void getRecentTasksAsync_getRecentTaskIds_returnsAllInOrder_fetchedTaskIds() {
177         mRecentTasksProvider.setRecentsDataChangeListener(
178                 new ConvenienceRecentsDataChangeListener() {
179                     @Override
180                     public void recentTasksFetched() {
181                         List<Integer> ret = mRecentTasksProvider.getRecentTaskIds();
182 
183                         assertThat(ret).isNotNull();
184                         assertThat(ret.size()).isEqualTo(RECENT_TASKS_LENGTH);
185                         for (int i = 0; i < RECENT_TASKS_LENGTH; i++) {
186                             assertThat(ret.get(i)).isEqualTo(i);
187                         }
188                     }
189                 });
190 
191         mRecentTasksProvider.getRecentTasksAsync();
192     }
193 
194     @Test
getRecentTasksAsync_getRecentTaskIds_filters_TYPE_SPLIT()195     public void getRecentTasksAsync_getRecentTaskIds_filters_TYPE_SPLIT() throws
196             RemoteException {
197         initRecentTaskList(/* addTypeSplit= */ true, /* addTypeFreeform= */ false);
198         assertThat(mGroupedRecentTaskInfo.length).isEqualTo(
199                 RECENT_TASKS_LENGTH + SPLIT_RECENT_TASKS_LENGTH);
200         when(mRecentTaskProxy.getRecentTasks(anyInt(), eq(RECENT_IGNORE_UNAVAILABLE),
201                 anyInt())).thenReturn(mGroupedRecentTaskInfo);
202 
203         mRecentTasksProvider.setRecentsDataChangeListener(
204                 new ConvenienceRecentsDataChangeListener() {
205                     @Override
206                     public void recentTasksFetched() {
207                         List<Integer> ret = mRecentTasksProvider.getRecentTaskIds();
208 
209                         assertThat(ret).isNotNull();
210                         assertThat(ret.size()).isEqualTo(RECENT_TASKS_LENGTH);
211                     }
212                 });
213 
214         mRecentTasksProvider.getRecentTasksAsync();
215     }
216 
217     @Test
getRecentTasksAsync_getRecentTaskIds_filters_TYPE_FREEFORM()218     public void getRecentTasksAsync_getRecentTaskIds_filters_TYPE_FREEFORM() throws
219             RemoteException {
220         initRecentTaskList(/* addTypeSplit= */ false, /* addTypeFreeform= */ true);
221         assertThat(mGroupedRecentTaskInfo.length).isEqualTo(
222                 RECENT_TASKS_LENGTH + FREEFORM_RECENT_TASKS_LENGTH);
223         when(mRecentTaskProxy.getRecentTasks(anyInt(), eq(RECENT_IGNORE_UNAVAILABLE),
224                 anyInt())).thenReturn(mGroupedRecentTaskInfo);
225 
226         mRecentTasksProvider.setRecentsDataChangeListener(
227                 new ConvenienceRecentsDataChangeListener() {
228                     @Override
229                     public void recentTasksFetched() {
230                         List<Integer> ret = mRecentTasksProvider.getRecentTaskIds();
231 
232                         assertThat(ret).isNotNull();
233                         assertThat(ret.size()).isEqualTo(RECENT_TASKS_LENGTH);
234                     }
235                 });
236 
237         mRecentTasksProvider.getRecentTasksAsync();
238     }
239 
240     @Test
getRecentTaskIconAsync_sets_iconFromTaskDescription()241     public void getRecentTaskIconAsync_sets_iconFromTaskDescription() {
242         int taskId = 500;
243         when(mTaskDescription.getInMemoryIcon()).thenReturn(mIconBitmap);
244         mTask.taskDescription = mTaskDescription;
245         when(mTaskKey.getComponent()).thenReturn(mComponent);
246         mTask.key = mTaskKey;
247         mRecentTasksProvider.mRecentTaskIdToTaskMap.put(taskId, mTask);
248 
249         mRecentTasksProvider.setRecentsDataChangeListener(
250                 new ConvenienceRecentsDataChangeListener() {
251                     @Override
252                     public void recentTaskIconChange(int taskId) {
253                         Drawable d = mRecentTasksProvider.getRecentTaskIcon(taskId);
254 
255                         assertThat(d instanceof BitmapDrawable).isTrue();
256                         assertThat(((BitmapDrawable) d).getBitmap()).isEqualTo(mIconBitmap);
257                     }
258                 });
259 
260         mRecentTasksProvider.getRecentTaskIconAsync(taskId);
261     }
262 
263     @Test
getRecentTaskIconAsync_sets_iconFromPackageManager()264     public void getRecentTaskIconAsync_sets_iconFromPackageManager() {
265         int taskId = 500;
266         when(mTaskDescription.getInMemoryIcon()).thenReturn(null);
267         mTask.taskDescription = mTaskDescription;
268         when(mTaskKey.getComponent()).thenReturn(mComponent);
269         mTask.key = mTaskKey;
270         when(mActivityInfo.loadIcon(any(PackageManager.class))).thenReturn(mIconDrawable);
271         when(mPackageManagerWrapper.getActivityInfo(eq(mComponent), anyInt())).thenReturn(
272                 mActivityInfo);
273         mRecentTasksProvider.mRecentTaskIdToTaskMap.put(taskId, mTask);
274 
275         mRecentTasksProvider.setRecentsDataChangeListener(
276                 new ConvenienceRecentsDataChangeListener() {
277                     @Override
278                     public void recentTaskIconChange(int taskId) {
279                         Drawable d = mRecentTasksProvider.getRecentTaskIcon(taskId);
280 
281                         assertThat(d).isEqualTo(mIconDrawable);
282                     }
283                 });
284 
285         mRecentTasksProvider.getRecentTaskIconAsync(taskId);
286     }
287 
288     @Test
getRecentTaskIconAsync_sets_defaultIcon()289     public void getRecentTaskIconAsync_sets_defaultIcon() {
290         int taskId = 500;
291         when(mTaskDescription.getInMemoryIcon()).thenReturn(null);
292         mTask.taskDescription = mTaskDescription;
293         when(mTaskKey.getComponent()).thenReturn(mComponent);
294         mTask.key = mTaskKey;
295         when(mPackageManagerWrapper.getActivityInfo(eq(mComponent), anyInt())).thenReturn(null);
296         mRecentTasksProvider.mRecentTaskIdToTaskMap.put(taskId, mTask);
297         mRecentTasksProvider.setDefaultIcon(mDefaultIconDrawable);
298 
299         mRecentTasksProvider.setRecentsDataChangeListener(
300                 new ConvenienceRecentsDataChangeListener() {
301                     @Override
302                     public void recentTaskIconChange(int taskId) {
303                         Drawable d = mRecentTasksProvider.getRecentTaskIcon(taskId);
304 
305                         assertThat(d).isEqualTo(mDefaultIconDrawable);
306                     }
307                 });
308 
309         mRecentTasksProvider.getRecentTaskIconAsync(taskId);
310     }
311 
312     @Test
openTopRunningTask_openRunningTaskAfterRecentsActivity()313     public void openTopRunningTask_openRunningTaskAfterRecentsActivity() {
314         int displayId = 0;
315         int tasksBeforeRecents = 2;
316         int tasksAfterRecents = 2;
317         ActivityManager.RunningTaskInfo[] infos = createRunningTaskList(
318                 tasksBeforeRecents, /* addRecentsClass= */ true, tasksAfterRecents,
319                 /* recentsClazz= */ RECENTS_ACTIVITY.class.getName());
320         when(mActivityManagerWrapper.getRunningTasks(anyBoolean(), eq(displayId)))
321                 .thenReturn(infos);
322         ActivityManager.RunningTaskInfo taskAfterRecents = infos[tasksBeforeRecents + 1];
323 
324         mRecentTasksProvider.openTopRunningTask(RECENTS_ACTIVITY.class, displayId);
325 
326         verify(mActivityManagerWrapper).startActivityFromRecents(eq(taskAfterRecents.taskId),
327                 nullable(ActivityOptions.class));
328     }
329 
330     @Test
openTopRunningTask_recentsActivityNotFound_noOp()331     public void openTopRunningTask_recentsActivityNotFound_noOp() {
332         int displayId = 0;
333         int tasksBeforeRecents = 2;
334         ActivityManager.RunningTaskInfo[] infos = createRunningTaskList(
335                 tasksBeforeRecents, /* addRecentsClass= */ false, /* tasksAfterRecents= */ 0,
336                 /* recentsClazz= */ RECENTS_ACTIVITY.class.getName());
337         when(mActivityManagerWrapper.getRunningTasks(anyBoolean(), eq(displayId)))
338                 .thenReturn(infos);
339 
340         mRecentTasksProvider.openTopRunningTask(RECENTS_ACTIVITY.class, displayId);
341 
342         verify(mActivityManagerWrapper, never()).startActivityFromRecents(anyInt(),
343                 nullable(ActivityOptions.class));
344     }
345 
346     @Test
openTopRunningTask_recentsActivityNotFound_returnsFalse()347     public void openTopRunningTask_recentsActivityNotFound_returnsFalse() {
348         int displayId = 0;
349         int tasksBeforeRecents = 2;
350         ActivityManager.RunningTaskInfo[] infos = createRunningTaskList(
351                 tasksBeforeRecents, /* addRecentsClass= */ false, /* tasksAfterRecents= */ 0,
352                 /* recentsClazz= */ RECENTS_ACTIVITY.class.getName());
353         when(mActivityManagerWrapper.getRunningTasks(anyBoolean(), eq(displayId)))
354                 .thenReturn(infos);
355 
356         boolean ret = mRecentTasksProvider.openTopRunningTask(RECENTS_ACTIVITY.class, displayId);
357 
358         assertThat(ret).isFalse();
359     }
360 
initRecentTaskList()361     private void initRecentTaskList() {
362         initRecentTaskList(/* addTypeSplit= */ false, /* addTypeFreeform= */ false);
363     }
364 
initRecentTaskList(boolean addTypeSplit, boolean addTypeFreeform)365     private void initRecentTaskList(boolean addTypeSplit, boolean addTypeFreeform) {
366         List<GroupedRecentTaskInfo> groupedRecentTaskInfos = new ArrayList<>();
367         for (int i = 0; i < RECENT_TASKS_LENGTH; i++) {
368             groupedRecentTaskInfos.add(
369                     createGroupedRecentTaskInfo(createRecentTaskInfo(i), TYPE_SINGLE));
370         }
371         if (addTypeSplit) {
372             for (int i = 0; i < SPLIT_RECENT_TASKS_LENGTH; i++) {
373                 groupedRecentTaskInfos.add(
374                         createGroupedRecentTaskInfo(createRecentTaskInfo(i), TYPE_SPLIT));
375             }
376         }
377         if (addTypeFreeform) {
378             for (int i = 0; i < FREEFORM_RECENT_TASKS_LENGTH; i++) {
379                 groupedRecentTaskInfos.add(
380                         createGroupedRecentTaskInfo(createRecentTaskInfo(i), TYPE_FREEFORM));
381             }
382         }
383         mGroupedRecentTaskInfo = groupedRecentTaskInfos.toArray(GroupedRecentTaskInfo[]::new);
384     }
385 
createGroupedRecentTaskInfo(ActivityManager.RecentTaskInfo info, int type)386     private GroupedRecentTaskInfo createGroupedRecentTaskInfo(ActivityManager.RecentTaskInfo info,
387             int type) {
388         GroupedRecentTaskInfo groupedRecentTaskInfo = mock(GroupedRecentTaskInfo.class);
389         when(groupedRecentTaskInfo.getType()).thenReturn(type);
390         when(groupedRecentTaskInfo.getTaskInfo1()).thenReturn(info);
391         return groupedRecentTaskInfo;
392     }
393 
createRecentTaskInfo(int taskId)394     private ActivityManager.RecentTaskInfo createRecentTaskInfo(int taskId) {
395         when(mBaseIntent.getComponent()).thenReturn(mComponent);
396         ActivityManager.RecentTaskInfo recentTaskInfo = new ActivityManager.RecentTaskInfo();
397         recentTaskInfo.taskId = taskId;
398         recentTaskInfo.taskDescription = mock(ActivityManager.TaskDescription.class);
399         recentTaskInfo.baseIntent = mBaseIntent;
400         return recentTaskInfo;
401     }
402 
createRunningTaskList(int tasksBeforeRecents, boolean addRecentsClass, int tasksAfterRecents, String recentsClazz)403     private ActivityManager.RunningTaskInfo[] createRunningTaskList(int tasksBeforeRecents,
404             boolean addRecentsClass, int tasksAfterRecents, String recentsClazz) {
405         int length =
406                 addRecentsClass ? tasksBeforeRecents + 1 + tasksAfterRecents : tasksBeforeRecents;
407         ActivityManager.RunningTaskInfo[] infos = new ActivityManager.RunningTaskInfo[length];
408         for (int i = 0; i < length; i++) {
409             ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class);
410             info.taskId = i;
411             if (i == tasksBeforeRecents) {
412                 info.topActivity = new ComponentName("pkg-" + i, recentsClazz);
413             } else {
414                 info.topActivity = new ComponentName("pkg-" + i, "class-" + i);
415             }
416             infos[i] = info;
417         }
418         return infos;
419     }
420 
421     private abstract static class ConvenienceRecentsDataChangeListener implements
422             RecentTasksProviderInterface.RecentsDataChangeListener {
423         @Override
recentTasksFetched()424         public void recentTasksFetched() {
425             // no-op
426         }
427 
428         @Override
recentTaskThumbnailChange(int taskId)429         public void recentTaskThumbnailChange(int taskId) {
430             // no-op
431         }
432 
433         @Override
recentTaskIconChange(int taskId)434         public void recentTaskIconChange(int taskId) {
435             // no-op
436         }
437     }
438 
439     private static class RECENTS_ACTIVITY extends Activity {
440     }
441 }
442