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