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.view.Display.DEFAULT_DISPLAY;
20 
21 import android.content.ComponentName;
22 import android.content.Intent;
23 import android.graphics.Bitmap;
24 import android.graphics.Canvas;
25 import android.graphics.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.view.View;
28 
29 import androidx.annotation.ColorInt;
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 
40 public class RecentTasksViewModel {
41     private static final RecentsStatsLogHelper sStatsLogHelper =
42             RecentsStatsLogHelper.getInstance();
43     private static RecentTasksViewModel sInstance;
44     private final RecentTasksProviderInterface mDataStore;
45     private final Set<RecentTasksChangeListener> mRecentTasksChangeListener;
46     private final Set<HiddenTaskProvider> mHiddenTaskProviders;
47     private DisabledTaskProvider mDisabledTaskProvider;
48     private final RecentTasksProviderInterface.RecentsDataChangeListener
49             mRecentsDataChangeListener =
50             new RecentTasksProviderInterface.RecentsDataChangeListener() {
51                 @Override
52                 public void recentTasksFetched() {
53                     mapAbsoluteTasksToShownTasks();
54                     mRecentTasksChangeListener.forEach(
55                             RecentTasksChangeListener::onRecentTasksFetched);
56                 }
57 
58                 @Override
59                 public void recentTaskThumbnailChange(int taskId) {
60                     int index = mRecentTaskIds.indexOf(taskId);
61                     if (index == -1) {
62                         return;
63                     }
64                     mTaskIdToCroppedThumbnailMap.remove(taskId);
65                     mRecentTasksChangeListener.forEach(l -> l.onTaskThumbnailChange(index));
66                 }
67 
68                 @Override
69                 public void recentTaskIconChange(int taskId) {
70                     int index = mRecentTaskIds.indexOf(taskId);
71                     if (index == -1) {
72                         return;
73                     }
74                     mRecentTasksChangeListener.forEach(l ->
75                             l.onTaskIconChange(index));
76                 }
77             };
78     private List<Integer> mRecentTaskIds;
79     private final Map<Integer, Bitmap> mTaskIdToCroppedThumbnailMap;
80     private Bitmap mDefaultThumbnail;
81     private boolean isInitialised;
82     private int mDisplayId = DEFAULT_DISPLAY;
83     private int mWindowWidth;
84     private int mWindowHeight;
85     private Rect mWindowInsets;
86 
RecentTasksViewModel()87     private RecentTasksViewModel() {
88         mDataStore = RecentTasksProvider.getInstance();
89         mRecentTasksChangeListener = new HashSet<>();
90         mHiddenTaskProviders = new HashSet<>();
91         mRecentTaskIds = new ArrayList<>();
92         mTaskIdToCroppedThumbnailMap = new HashMap<>();
93     }
94 
95     /**
96      * Initialise connections and setup configs
97      *
98      * @param displayId             the display on which the recents activity is displayed.
99      * @param windowWidth           width of window on which recent activity is displayed.
100      * @param windowHeight          height of window on which recent activity is displayed.
101      * @param windowInsets          insets of window on which recent activity is displayed.
102      * @param defaultThumbnailColor color of the default recent task thumbnail to be shown when
103      *                              thumbnail is not loaded or not present.
104      */
init(int displayId, int windowWidth, int windowHeight, @NonNull Rect windowInsets, @ColorInt Integer defaultThumbnailColor)105     public void init(int displayId, int windowWidth, int windowHeight, @NonNull Rect windowInsets,
106             @ColorInt Integer defaultThumbnailColor) {
107         if (isInitialised) {
108             return;
109         }
110         isInitialised = true;
111         mDataStore.setRecentsDataChangeListener(mRecentsDataChangeListener);
112         mDisplayId = displayId;
113         mWindowWidth = windowWidth;
114         mWindowHeight = windowHeight;
115         mWindowInsets = windowInsets;
116         mDefaultThumbnail = createThumbnail(defaultThumbnailColor);
117     }
118 
119     /**
120      * Terminates connections and removes all {@link RecentTasksChangeListener}s and
121      * {@link HiddenTaskProvider}s.
122      */
terminate()123     public void terminate() {
124         isInitialised = false;
125         mDataStore.setRecentsDataChangeListener(/* listener= */ null);
126         mRecentTasksChangeListener.clear();
127         mHiddenTaskProviders.clear();
128         mDisabledTaskProvider = null;
129     }
130 
getInstance()131     public static RecentTasksViewModel getInstance() {
132         if (sInstance == null) {
133             sInstance = new RecentTasksViewModel();
134         }
135         return sInstance;
136     }
137 
138     /**
139      * Fetches recent task list asynchronously and communicates changes through
140      * {@link RecentTasksChangeListener}.
141      */
fetchRecentTaskList()142     public void fetchRecentTaskList() {
143         mDataStore.getRecentTasksAsync();
144     }
145 
146     /**
147      * Refreshes the UI associated with recent tasks.
148      * Does not fetch recent task list from the system.
149      */
refreshRecentTaskList()150     public void refreshRecentTaskList() {
151         mRecentTasksChangeListener.forEach(RecentTasksChangeListener::onRecentTasksFetched);
152     }
153 
154     /**
155      * @return the {@link Drawable} icon for the given {@code index} or null.
156      */
157     @Nullable
getRecentTaskIconAt(int index)158     public Drawable getRecentTaskIconAt(int index) {
159         if (!safeCheckIndex(mRecentTaskIds, index)) {
160             return null;
161         }
162         return mDataStore.getRecentTaskIcon(mRecentTaskIds.get(index));
163     }
164 
165     /**
166      * @return the {@link Bitmap} thumbnail for the given {@code index} or
167      * default thumbnail(which could be null of not initialised).
168      */
169     @Nullable
getRecentTaskThumbnailAt(int index)170     public Bitmap getRecentTaskThumbnailAt(int index) {
171         if (!safeCheckIndex(mRecentTaskIds, index)) {
172             return null;
173         }
174         if (mTaskIdToCroppedThumbnailMap.containsKey(mRecentTaskIds.get(index))) {
175             return mTaskIdToCroppedThumbnailMap.get(mRecentTaskIds.get(index));
176         }
177         Bitmap thumbnail = mDataStore.getRecentTaskThumbnail(mRecentTaskIds.get(index));
178         Rect insets = mDataStore.getRecentTaskInsets(mRecentTaskIds.get(index));
179         if (thumbnail != null) {
180             Bitmap croppedThumbnail = cropInsets(thumbnail, insets);
181             mTaskIdToCroppedThumbnailMap.put(mRecentTaskIds.get(index), croppedThumbnail);
182             return croppedThumbnail;
183         }
184         return mDefaultThumbnail;
185     }
186 
187     /**
188      * @return {@code true} if task for the given {@code index} is disabled.
189      */
isRecentTaskDisabled(int index)190     public boolean isRecentTaskDisabled(int index) {
191         if (mDisabledTaskProvider == null) {
192             return false;
193         }
194         ComponentName componentName = getRecentTaskComponentName(index);
195         return componentName != null &&
196                 mDisabledTaskProvider.isTaskDisabledFromRecents(componentName);
197     }
198 
199     /**
200      * @return the {@link View.OnClickListener} for the task at the given {@code index} or null.
201      */
202     @Nullable
getDisabledTaskClickListener(int index)203     public View.OnClickListener getDisabledTaskClickListener(int index) {
204         if (mDisabledTaskProvider == null) {
205             return null;
206         }
207         ComponentName componentName = getRecentTaskComponentName(index);
208         return componentName != null
209                 ? mDisabledTaskProvider.getDisabledTaskClickListener(componentName) : null;
210     }
211 
212     @Nullable
getRecentTaskComponentName(int index)213     private ComponentName getRecentTaskComponentName(int index) {
214         if (!safeCheckIndex(mRecentTaskIds, index)) {
215             return null;
216         }
217         return mDataStore.getRecentTaskComponentName(mRecentTaskIds.get(index));
218     }
219 
220     /**
221      * Tries to open the recent task at the given {@code index}.
222      * Communicates failure through {@link RecentTasksChangeListener}.
223      */
openRecentTask(int index)224     public void openRecentTask(int index) {
225         if (safeCheckIndex(mRecentTaskIds, index) &&
226                 mDataStore.openRecentTask(mRecentTaskIds.get(index))) {
227             // TODO(b/311427536): log a boolean to indicate if openRecentTask finished successfully
228             ComponentName name = mDataStore.getRecentTaskComponentName(mRecentTaskIds.get(index));
229             sStatsLogHelper.logAppLaunched(/* totalTaskCount */ getRecentTasksSize(),
230                     /* eventTaskIndex */ index, /* componentName */ name.getPackageName());
231             return;
232         }
233         // failure to open recent task
234         mRecentTasksChangeListener.forEach(RecentTasksChangeListener::onOpenRecentTaskFail);
235     }
236 
237     /**
238      * Tries to open the top running task.
239      * Communicates failure through {@link RecentTasksChangeListener}.
240      */
openMostRecentTask()241     public void openMostRecentTask() {
242         if (!mDataStore.openTopRunningTask(CarRecentsActivity.class, mDisplayId)) {
243             mRecentTasksChangeListener.forEach(RecentTasksChangeListener::onOpenTopRunningTaskFail);
244         }
245     }
246 
247     /**
248      * Communicates success through {@link RecentTasksChangeListener}.
249      *
250      * @param index index of the task to be removed from recents.
251      */
removeTaskFromRecents(int index)252     public void removeTaskFromRecents(int index) {
253         if (!safeCheckIndex(mRecentTaskIds, index)) {
254             return;
255         }
256         ComponentName name = getRecentTaskComponentName(index);
257         removeTaskWithId(mRecentTaskIds.get(index));
258         mRecentTaskIds.remove(index);
259         mRecentTasksChangeListener.forEach(l -> l.onRecentTaskRemoved(index));
260         sStatsLogHelper.logAppDismissed(/* totalTaskCount */ getRecentTasksSize(),
261                 /* eventTaskIndex */ index, /* packageName */ name.getPackageName());
262     }
263 
264     /**
265      * Removes all tasks from recents and clears cached data by calling {@link #clearCache}.
266      */
removeAllRecentTasks()267     public void removeAllRecentTasks() {
268         for (int recentTaskId : mRecentTaskIds) {
269             removeTaskWithId(recentTaskId);
270         }
271         sStatsLogHelper.logClearAll(getRecentTasksSize());
272         clearCache();
273     }
274 
275     /**
276      * Clears cached data.
277      * Communicates success through {@link RecentTasksChangeListener}.
278      */
clearCache()279     public void clearCache() {
280         mDataStore.clearCache();
281         mTaskIdToCroppedThumbnailMap.clear();
282         int countRemoved = mRecentTaskIds.size();
283         mRecentTaskIds.clear();
284         mRecentTasksChangeListener.forEach(l -> l.onAllRecentTasksRemoved(countRemoved));
285     }
286 
287     /**
288      * @return the length of the recent task list
289      */
getRecentTasksSize()290     public int getRecentTasksSize() {
291         return mRecentTaskIds.size();
292     }
293 
294     /**
295      * Used to map relative indexes to absolute indexes based on tasks hidden by
296      * {@link HiddenTaskProvider}.
297      */
mapAbsoluteTasksToShownTasks()298     private void mapAbsoluteTasksToShownTasks() {
299         List<Integer> recentTaskIds = mDataStore.getRecentTaskIds();
300         mRecentTaskIds = new ArrayList<>(recentTaskIds.size());
301         for (int taskId : recentTaskIds) {
302             ComponentName topComponent = mDataStore.getRecentTaskComponentName(taskId);
303             Intent baseIntent = mDataStore.getRecentTaskBaseIntent(taskId);
304             boolean isTaskHidden = mHiddenTaskProviders.stream()
305                     .anyMatch(p -> p.isTaskHiddenFromRecents(
306                             topComponent != null ? topComponent.getPackageName() : null,
307                             topComponent != null ? topComponent.getClassName() : null,
308                             baseIntent));
309             if (isTaskHidden) {
310                 // skip since it should be hidden
311                 continue;
312             }
313             mRecentTaskIds.add(taskId);
314         }
315     }
316 
317     @NonNull
cropInsets(Bitmap bitmap, Rect insets)318     private Bitmap cropInsets(Bitmap bitmap, Rect insets) {
319         return Bitmap.createBitmap(bitmap, insets.left, insets.top,
320                 /* width= */ bitmap.getWidth() - insets.left - insets.right,
321                 /* height= */ bitmap.getHeight() - insets.top - insets.bottom);
322     }
323 
324     /**
325      * @return a new {@link Bitmap} with aspect ratio of the current window and the given
326      * {@code color}.
327      */
createThumbnail(@olorInt Integer color)328     public Bitmap createThumbnail(@ColorInt Integer color) {
329         return createThumbnail(mWindowWidth, mWindowHeight, mWindowInsets, color);
330     }
331 
createThumbnail(int width, int height, @NonNull Rect insets, @ColorInt Integer color)332     private Bitmap createThumbnail(int width, int height, @NonNull Rect insets,
333             @ColorInt Integer color) {
334         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
335         Canvas canvas = new Canvas(bitmap);
336         canvas.drawColor(color);
337         return cropInsets(bitmap, insets);
338     }
339 
safeCheckIndex(List<?> list, int index)340     private boolean safeCheckIndex(List<?> list, int index) {
341         return index >= 0 && index < list.size();
342     }
343 
removeTaskWithId(int taskId)344     private void removeTaskWithId(int taskId) {
345         mDataStore.removeTaskFromRecents(taskId);
346         mTaskIdToCroppedThumbnailMap.remove(taskId);
347     }
348 
349     /**
350      * @param listener listener to send changes in recent task list to.
351      */
addRecentTasksChangeListener(RecentTasksChangeListener listener)352     public void addRecentTasksChangeListener(RecentTasksChangeListener listener) {
353         mRecentTasksChangeListener.add(listener);
354     }
355 
356     /**
357      * @param listener remove the given listener.
358      */
removeAllRecentTasksChangeListeners(RecentTasksChangeListener listener)359     public void removeAllRecentTasksChangeListeners(RecentTasksChangeListener listener) {
360         mRecentTasksChangeListener.remove(listener);
361     }
362 
363     /**
364      * @param provider provider of packages to be hidden from recents.
365      */
addHiddenTaskProvider(HiddenTaskProvider provider)366     public void addHiddenTaskProvider(HiddenTaskProvider provider) {
367         mHiddenTaskProviders.add(provider);
368     }
369 
370     /**
371      * @param provider remove the given provider.
372      */
removeHiddenTaskProvider(HiddenTaskProvider provider)373     public void removeHiddenTaskProvider(HiddenTaskProvider provider) {
374         mHiddenTaskProviders.remove(provider);
375     }
376 
377     /**
378      * @param provider provider of packages to be disabled in recents.
379      */
setDisabledTaskProvider(DisabledTaskProvider provider)380     public void setDisabledTaskProvider(DisabledTaskProvider provider) {
381         mDisabledTaskProvider = provider;
382     }
383 
384     /**
385      * Listen to changes in the recents.
386      */
387     public interface RecentTasksChangeListener {
388         /**
389          * Called when recent tasks have been fetched from the system.
390          */
onRecentTasksFetched()391         default void onRecentTasksFetched() {
392         }
393 
394         /**
395          * @param position position whose thumbnail has been changed.
396          */
onTaskThumbnailChange(int position)397         default void onTaskThumbnailChange(int position) {
398         }
399 
400         /**
401          * @param position position whose icon has been changed.
402          */
onTaskIconChange(int position)403         default void onTaskIconChange(int position) {
404         }
405 
406         /**
407          * Called when system fails to open a recent task.
408          */
onOpenRecentTaskFail()409         default void onOpenRecentTaskFail() {
410         }
411 
412         /**
413          * Called when system fails to open the top task.
414          */
onOpenTopRunningTaskFail()415         default void onOpenTopRunningTaskFail() {
416         }
417 
418         /**
419          * @param countRemoved number of recent tasks removed.
420          */
onAllRecentTasksRemoved(int countRemoved)421         default void onAllRecentTasksRemoved(int countRemoved) {
422         }
423 
424         /**
425          * @param position position at which the recent task was removed.
426          */
onRecentTaskRemoved(int position)427         default void onRecentTaskRemoved(int position) {
428         }
429     }
430 
431     /**
432      * Decides if a task should be hidden from recents.
433      * This is necessary to be able to get tasks to be hidden at runtime.
434      */
435     public interface HiddenTaskProvider {
436         /**
437          * @return if the task should be hidden from recents.
438          */
isTaskHiddenFromRecents(String packageName, String className, Intent baseIntent)439         boolean isTaskHiddenFromRecents(String packageName, String className, Intent baseIntent);
440     }
441 
442     /**
443      * Decides if a task is disabled in recents.
444      * This is necessary to be able to get tasks to be disabled at runtime.
445      * Note: Hidden tasks cannot be disabled.
446      */
447     public interface DisabledTaskProvider {
448         /**
449          * @return if the task associated with {@code componentName} is disabled in recents.
450          */
isTaskDisabledFromRecents(ComponentName componentName)451         boolean isTaskDisabledFromRecents(ComponentName componentName);
452 
453         /**
454          * @return {@link View.OnClickListener} to be called when user tries to click on
455          * disabled task associated with {@code componentName}.
456          */
getDisabledTaskClickListener(ComponentName componentName)457         default View.OnClickListener getDisabledTaskClickListener(ComponentName componentName) {
458             return null;
459         }
460     }
461 }
462