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 android.app.Activity;
26 import android.app.ActivityManager;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.ActivityInfo;
31 import android.content.res.Resources;
32 import android.graphics.Bitmap;
33 import android.graphics.Rect;
34 import android.graphics.drawable.BitmapDrawable;
35 import android.graphics.drawable.Drawable;
36 import android.os.Build;
37 import android.os.Handler;
38 import android.os.Looper;
39 import android.os.RemoteException;
40 import android.util.Log;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 
45 import com.android.systemui.shared.recents.model.Task;
46 import com.android.systemui.shared.recents.model.ThumbnailData;
47 import com.android.systemui.shared.system.ActivityManagerWrapper;
48 import com.android.systemui.shared.system.PackageManagerWrapper;
49 import com.android.systemui.shared.system.TaskStackChangeListener;
50 import com.android.systemui.shared.system.TaskStackChangeListeners;
51 import com.android.wm.shell.recents.IRecentTasks;
52 import com.android.wm.shell.util.GroupedRecentTaskInfo;
53 
54 import com.google.common.annotations.VisibleForTesting;
55 
56 import java.util.ArrayList;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Objects;
61 import java.util.concurrent.Executor;
62 import java.util.concurrent.Executors;
63 
64 public class RecentTasksProvider implements RecentTasksProviderInterface {
65     private static final String TAG = "RecentTasksProviderInterface";
66     private static final boolean DEBUG = Build.IS_DEBUGGABLE;
67     private static Executor sRecentsModelExecutor = Executors.newSingleThreadExecutor();
68     private static Handler sMainHandler = new Handler(Looper.getMainLooper());
69     private static RecentTasksProvider sInstance;
70     private Context mContext;
71     private IRecentTasks mRecentTasksProxy;
72     private ActivityManagerWrapper mActivityManagerWrapper;
73     private PackageManagerWrapper mPackageManagerWrapper;
74     private Drawable mDefaultIcon;
75     private List<Integer> mRecentTaskIds;
76     @VisibleForTesting
77     Map<Integer, Task> mRecentTaskIdToTaskMap;
78     private RecentsDataChangeListener mRecentsDataChangeListener;
79     private boolean mIsInitialised;
80     private final TaskStackChangeListener mTaskStackChangeListener = new TaskStackChangeListener() {
81         @Override
82         public boolean onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
83             if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
84                 return false;
85             }
86             mRecentTaskIdToTaskMap.get(taskId).thumbnail = snapshot;
87             if (mRecentsDataChangeListener != null) {
88                 sMainHandler.post(
89                         () -> mRecentsDataChangeListener.recentTaskThumbnailChange(taskId));
90             }
91             return true;
92         }
93 
94         @Override
95         public void onTaskDescriptionChanged(ActivityManager.RunningTaskInfo taskInfo) {
96             if (!mRecentTaskIdToTaskMap.containsKey(taskInfo.taskId)) {
97                 return;
98             }
99             Drawable icon = getIconFromTaskDescription(taskInfo.taskDescription);
100             if (icon == null) {
101                 return;
102             }
103             mRecentTaskIdToTaskMap.get(taskInfo.taskId).icon = icon;
104             if (mRecentsDataChangeListener != null) {
105                 sMainHandler.post(
106                         () -> mRecentsDataChangeListener.recentTaskIconChange(taskInfo.taskId));
107             }
108         }
109     };
110 
RecentTasksProvider()111     private RecentTasksProvider() {
112         mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
113         mPackageManagerWrapper = PackageManagerWrapper.getInstance();
114         mRecentTaskIds = new ArrayList<>();
115         mRecentTaskIdToTaskMap = new HashMap<>();
116         mDefaultIcon = Objects.requireNonNull(Resources.getSystem().getDrawable(
117                 android.R.drawable.sym_def_app_icon, /* theme= */ null));
118     }
119 
getInstance()120     public static RecentTasksProvider getInstance() {
121         if (sInstance == null) {
122             sInstance = new RecentTasksProvider();
123         }
124         return sInstance;
125     }
126 
init(Context context, IRecentTasks recentTasksProxy)127     public void init(Context context, IRecentTasks recentTasksProxy) {
128         if (mIsInitialised) {
129             return;
130         }
131         mIsInitialised = true;
132         mContext = context;
133         mRecentTasksProxy = recentTasksProxy;
134         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackChangeListener);
135     }
136 
137     /**
138      * Terminates connections and sets shared service variables to {@code null}.
139      */
terminate()140     public void terminate() {
141         mIsInitialised = false;
142         mContext = null;
143         mRecentTasksProxy = null;
144         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
145                 mTaskStackChangeListener);
146         clearCache();
147     }
148 
149     @Override
getRecentTasksAsync()150     public void getRecentTasksAsync() {
151         if (mRecentTasksProxy == null) {
152             return;
153         }
154         sRecentsModelExecutor.execute(() -> {
155             GroupedRecentTaskInfo[] groupedRecentTasks;
156             try {
157                 // todo: b/271498799 use ActivityManagerWrapper.getInstance().getCurrentUserId()
158                 //  or equivalent instead of hidden API mContext.getUserId()
159                 groupedRecentTasks =
160                         mRecentTasksProxy.getRecentTasks(Integer.MAX_VALUE,
161                                 RECENT_IGNORE_UNAVAILABLE, mContext.getUserId());
162             } catch (RemoteException e) {
163                 if (DEBUG) {
164                     Log.e(TAG, e.toString());
165                 }
166                 return;
167             }
168             if (groupedRecentTasks == null) {
169                 return;
170             }
171             mRecentTaskIds = new ArrayList<>(groupedRecentTasks.length);
172             mRecentTaskIdToTaskMap = new HashMap<>(groupedRecentTasks.length);
173             boolean areSplitOrFreeformTypeTasksPresent = false;
174             for (GroupedRecentTaskInfo groupedRecentTask : groupedRecentTasks) {
175                 switch (groupedRecentTask.getType()) {
176                     case TYPE_SINGLE:
177                         // Automotive doesn't have split screen functionality, only process tasks
178                         // of TYPE_SINGLE.
179                         ActivityManager.RecentTaskInfo taskInfo = groupedRecentTask.getTaskInfo1();
180                         Task.TaskKey taskKey = new Task.TaskKey(taskInfo);
181 
182                         // isLocked is always set to false since this value is not required in
183                         // automotive. Usually set as Keyguard lock state for the user associated
184                         // with the task.
185                         // ag/1705623 introduced this to support multiple profiles under same user
186                         // where this value is necessary to check if profile user associated with
187                         // the task is unlocked.
188                         Task task = Task.from(taskKey, taskInfo, /* isLocked= */ false);
189                         task.setLastSnapshotData(taskInfo);
190                         mRecentTaskIds.add(task.key.id);
191                         mRecentTaskIdToTaskMap.put(task.key.id, task);
192                         getRecentTaskThumbnailAsync(task.key.id);
193                         getRecentTaskIconAsync(task.key.id);
194                         break;
195                     case TYPE_SPLIT:
196                     case TYPE_FREEFORM:
197                         areSplitOrFreeformTypeTasksPresent = true;
198                 }
199             }
200             if (areSplitOrFreeformTypeTasksPresent && DEBUG) {
201                 Log.d(TAG, "Automotive doesn't support TYPE_SPLIT and TYPE_FREEFORM tasks");
202             }
203             if (mRecentsDataChangeListener != null) {
204                 sMainHandler.post(() -> mRecentsDataChangeListener.recentTasksFetched());
205             }
206         });
207     }
208 
209     @NonNull
210     @Override
getRecentTaskIds()211     public List<Integer> getRecentTaskIds() {
212         return new ArrayList<>(mRecentTaskIds);
213     }
214 
215     @Nullable
216     @Override
getRecentTaskComponentName(int taskId)217     public ComponentName getRecentTaskComponentName(int taskId) {
218         return mRecentTaskIdToTaskMap.containsKey(taskId)
219                 ? mRecentTaskIdToTaskMap.get(taskId).getTopComponent() : null;
220     }
221 
222     @Nullable
223     @Override
getRecentTaskBaseIntent(int taskId)224     public Intent getRecentTaskBaseIntent(int taskId) {
225         return mRecentTaskIdToTaskMap.containsKey(taskId)
226                 ? mRecentTaskIdToTaskMap.get(taskId).getKey().baseIntent : null;
227     }
228 
229     @Nullable
230     @Override
getRecentTaskIcon(int taskId)231     public Drawable getRecentTaskIcon(int taskId) {
232         return mRecentTaskIdToTaskMap.containsKey(taskId)
233                 ? mRecentTaskIdToTaskMap.get(taskId).icon : null;
234     }
235 
236     @Nullable
237     @Override
getRecentTaskThumbnail(int taskId)238     public Bitmap getRecentTaskThumbnail(int taskId) {
239         ThumbnailData thumbnailData = getRecentTaskThumbnailData(taskId);
240         return thumbnailData != null ? thumbnailData.getThumbnail() : null;
241     }
242 
243     @NonNull
244     @Override
getRecentTaskInsets(int taskId)245     public Rect getRecentTaskInsets(int taskId) {
246         ThumbnailData thumbnailData = getRecentTaskThumbnailData(taskId);
247         return thumbnailData != null ? thumbnailData.insets : new Rect();
248 
249     }
250 
251     @Nullable
getRecentTaskThumbnailData(int taskId)252     private ThumbnailData getRecentTaskThumbnailData(int taskId) {
253         if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
254             return null;
255         }
256         return mRecentTaskIdToTaskMap.get(taskId).thumbnail;
257     }
258 
259     @Override
openRecentTask(int taskId)260     public boolean openRecentTask(int taskId) {
261         if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
262             return false;
263         }
264         return ActivityManagerWrapper.getInstance().startActivityFromRecents(
265                 mRecentTaskIdToTaskMap.get(taskId).key, /* options= */ null);
266     }
267 
268     @Override
openTopRunningTask(@onNull Class<? extends Activity> recentsActivity, int displayId)269     public boolean openTopRunningTask(@NonNull Class<? extends Activity> recentsActivity,
270             int displayId) {
271         ActivityManager.RunningTaskInfo[] runningTasks = mActivityManagerWrapper.getRunningTasks(
272                 /* filterOnlyVisibleRecents= */ false, displayId);
273         boolean foundRecentsTask = false;
274         for (ActivityManager.RunningTaskInfo runningTask : runningTasks) {
275             if (runningTask == null) {
276                 return false;
277             }
278             if (foundRecentsTask) {
279                 // this is the running task after recents task, attempt to open
280                 return mActivityManagerWrapper.startActivityFromRecents(
281                         runningTask.taskId, /* options= */ null);
282             }
283             String topComponent = runningTask.topActivity != null
284                     ? runningTask.topActivity.getClassName()
285                     : runningTask.baseIntent.getComponent().getClassName();
286             if (recentsActivity.getName().equals(topComponent)) {
287                 foundRecentsTask = true;
288             }
289         }
290         // Recents task not found or no task present after recents task,
291         // not attempting to open any running task
292         return false;
293     }
294 
295     @Override
removeTaskFromRecents(int taskId)296     public void removeTaskFromRecents(int taskId) {
297         if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
298             return;
299         }
300         mActivityManagerWrapper.removeTask(mRecentTaskIdToTaskMap.get(taskId).key.id);
301         mRecentTaskIds.remove((Integer) taskId);
302     }
303 
304     @Override
removeAllRecentTasks()305     public void removeAllRecentTasks() {
306         mActivityManagerWrapper.removeAllRecentTasks();
307         clearCache();
308     }
309 
310     @Override
clearCache()311     public void clearCache() {
312         mRecentTaskIds.clear();
313         mRecentTaskIdToTaskMap.clear();
314     }
315 
316     @Override
setRecentsDataChangeListener(@ullable RecentsDataChangeListener listener)317     public void setRecentsDataChangeListener(@Nullable RecentsDataChangeListener listener) {
318         mRecentsDataChangeListener = listener;
319     }
320 
getRecentTaskThumbnailAsync(int taskId)321     private void getRecentTaskThumbnailAsync(int taskId) {
322         sRecentsModelExecutor.execute(() -> {
323             if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
324                 return;
325             }
326             ThumbnailData thumbnailData = mActivityManagerWrapper.getTaskThumbnail(
327                     taskId, /* isLowResolution= */ false);
328             mRecentTaskIdToTaskMap.get(taskId).thumbnail = thumbnailData;
329             if (mRecentsDataChangeListener != null) {
330                 sMainHandler.post(
331                         () -> mRecentsDataChangeListener.recentTaskThumbnailChange(taskId));
332             }
333         });
334     }
335 
336     @VisibleForTesting
getRecentTaskIconAsync(int taskId)337     void getRecentTaskIconAsync(int taskId) {
338         sRecentsModelExecutor.execute(() -> {
339             if (!mRecentTaskIdToTaskMap.containsKey(taskId)) {
340                 return;
341             }
342             Task task = mRecentTaskIdToTaskMap.get(taskId);
343             Task.TaskKey key = task.key;
344             Drawable drawableIcon = getIconFromTaskDescription(task.taskDescription);
345 
346             if (drawableIcon == null) {
347                 ActivityInfo activityInfo = mPackageManagerWrapper.getActivityInfo(
348                         key.getComponent(), key.userId);
349                 if (activityInfo != null) {
350                     drawableIcon = activityInfo.loadIcon(mContext.getPackageManager());
351                 } else {
352                     // set it a default icon
353                     drawableIcon = mDefaultIcon;
354                 }
355             }
356             mRecentTaskIdToTaskMap.get(taskId).icon = drawableIcon;
357             if (mRecentsDataChangeListener != null) {
358                 sMainHandler.post(
359                         () -> mRecentsDataChangeListener.recentTaskIconChange(taskId));
360             }
361         });
362     }
363 
364     @Nullable
getIconFromTaskDescription( ActivityManager.TaskDescription taskDescription)365     private Drawable getIconFromTaskDescription(
366             ActivityManager.TaskDescription taskDescription) {
367         Bitmap icon;
368         // todo: b/271498799 access through ActivityManagerWrapper instead of using
369         //  hidden api getInMemoryIcon(), loadTaskDescriptionIcon() and mContext.getUserId()
370         if (taskDescription.getInMemoryIcon() != null) {
371             icon = taskDescription.getInMemoryIcon();
372         } else {
373             icon = ActivityManager.TaskDescription.loadTaskDescriptionIcon(
374                     taskDescription.getIconFilename(), mContext.getUserId());
375         }
376         return icon != null ? new BitmapDrawable(mContext.getResources(), icon) : null;
377     }
378 
379     @VisibleForTesting
setExecutor(Executor executor)380     static void setExecutor(Executor executor) {
381         sRecentsModelExecutor = executor;
382     }
383 
384     @VisibleForTesting
setHandler(Handler handler)385     static void setHandler(Handler handler) {
386         sMainHandler = handler;
387     }
388 
389     @VisibleForTesting
setActivityManagerWrapper(ActivityManagerWrapper activityManagerWrapper)390     void setActivityManagerWrapper(ActivityManagerWrapper activityManagerWrapper) {
391         mActivityManagerWrapper = activityManagerWrapper;
392     }
393 
394     @VisibleForTesting
setPackageManagerWrapper(PackageManagerWrapper packageManagerWrapper)395     void setPackageManagerWrapper(PackageManagerWrapper packageManagerWrapper) {
396         mPackageManagerWrapper = packageManagerWrapper;
397     }
398 
399     @VisibleForTesting
setDefaultIcon(Drawable icon)400     void setDefaultIcon(Drawable icon) {
401         mDefaultIcon = icon;
402     }
403 }
404