1 /*
2  * Copyright (C) 2016 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 package com.android.launcher3.model;
17 
18 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY;
19 
20 import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
21 import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
22 
23 import static java.util.stream.Collectors.groupingBy;
24 import static java.util.stream.Collectors.mapping;
25 
26 import android.content.Context;
27 import android.content.pm.LauncherApps;
28 import android.content.pm.ShortcutInfo;
29 import android.os.UserHandle;
30 import android.text.TextUtils;
31 import android.util.ArraySet;
32 import android.util.Log;
33 
34 import androidx.annotation.Nullable;
35 
36 import com.android.launcher3.LauncherSettings;
37 import com.android.launcher3.LauncherSettings.Favorites;
38 import com.android.launcher3.Workspace;
39 import com.android.launcher3.config.FeatureFlags;
40 import com.android.launcher3.model.data.AppInfo;
41 import com.android.launcher3.model.data.FolderInfo;
42 import com.android.launcher3.model.data.ItemInfo;
43 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
44 import com.android.launcher3.model.data.WorkspaceItemInfo;
45 import com.android.launcher3.pm.UserCache;
46 import com.android.launcher3.shortcuts.ShortcutKey;
47 import com.android.launcher3.shortcuts.ShortcutRequest;
48 import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
49 import com.android.launcher3.util.ComponentKey;
50 import com.android.launcher3.util.IntArray;
51 import com.android.launcher3.util.IntSet;
52 import com.android.launcher3.util.IntSparseArrayMap;
53 import com.android.launcher3.util.ItemInfoMatcher;
54 import com.android.launcher3.util.RunnableList;
55 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
56 
57 import java.io.FileDescriptor;
58 import java.io.PrintWriter;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.Collections;
62 import java.util.HashMap;
63 import java.util.HashSet;
64 import java.util.Iterator;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.Set;
68 import java.util.function.Consumer;
69 import java.util.stream.Collectors;
70 import java.util.stream.Stream;
71 
72 /**
73  * All the data stored in-memory and managed by the LauncherModel
74  */
75 public class BgDataModel {
76 
77     private static final String TAG = "BgDataModel";
78 
79     /**
80      * Map of all the ItemInfos (shortcuts, folders, and widgets) created by
81      * LauncherModel to their ids
82      */
83     public final IntSparseArrayMap<ItemInfo> itemsIdMap = new IntSparseArrayMap<>();
84 
85     /**
86      * List of all the folders and shortcuts directly on the home screen (no widgets
87      * or shortcuts within folders).
88      */
89     public final ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
90 
91     /**
92      * All LauncherAppWidgetInfo created by LauncherModel.
93      */
94     public final ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
95 
96     /**
97      * Map of id to FolderInfos of all the folders created by LauncherModel
98      */
99     public final IntSparseArrayMap<FolderInfo> folders = new IntSparseArrayMap<>();
100 
101     /**
102      * Extra container based items
103      */
104     public final IntSparseArrayMap<FixedContainerItems> extraItems = new IntSparseArrayMap<>();
105 
106     /**
107      * Maps all launcher activities to counts of their shortcuts.
108      */
109     public final HashMap<ComponentKey, Integer> deepShortcutMap = new HashMap<>();
110 
111     /**
112      * Entire list of widgets.
113      */
114     public final WidgetsModel widgetsModel = new WidgetsModel();
115 
116     /**
117      * Id when the model was last bound
118      */
119     public int lastBindId = 0;
120 
121     /**
122      * Clears all the data
123      */
clear()124     public synchronized void clear() {
125         workspaceItems.clear();
126         appWidgets.clear();
127         folders.clear();
128         itemsIdMap.clear();
129         deepShortcutMap.clear();
130         extraItems.clear();
131     }
132 
133     /**
134      * Creates an array of valid workspace screens based on current items in the model.
135      */
collectWorkspaceScreens()136     public synchronized IntArray collectWorkspaceScreens() {
137         IntSet screenSet = new IntSet();
138         for (ItemInfo item: itemsIdMap) {
139             if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
140                 screenSet.add(item.screenId);
141             }
142         }
143         if (FeatureFlags.QSB_ON_FIRST_SCREEN || screenSet.isEmpty()) {
144             screenSet.add(Workspace.FIRST_SCREEN_ID);
145         }
146         return screenSet.getArray();
147     }
148 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)149     public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer,
150             String[] args) {
151         writer.println(prefix + "Data Model:");
152         writer.println(prefix + " ---- workspace items ");
153         for (int i = 0; i < workspaceItems.size(); i++) {
154             writer.println(prefix + '\t' + workspaceItems.get(i).toString());
155         }
156         writer.println(prefix + " ---- appwidget items ");
157         for (int i = 0; i < appWidgets.size(); i++) {
158             writer.println(prefix + '\t' + appWidgets.get(i).toString());
159         }
160         writer.println(prefix + " ---- folder items ");
161         for (int i = 0; i< folders.size(); i++) {
162             writer.println(prefix + '\t' + folders.valueAt(i).toString());
163         }
164         writer.println(prefix + " ---- items id map ");
165         for (int i = 0; i< itemsIdMap.size(); i++) {
166             writer.println(prefix + '\t' + itemsIdMap.valueAt(i).toString());
167         }
168 
169         if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
170             writer.println(prefix + "shortcut counts ");
171             for (Integer count : deepShortcutMap.values()) {
172                 writer.print(count + ", ");
173             }
174             writer.println();
175         }
176     }
177 
removeItem(Context context, ItemInfo... items)178     public synchronized void removeItem(Context context, ItemInfo... items) {
179         removeItem(context, Arrays.asList(items));
180     }
181 
removeItem(Context context, Iterable<? extends ItemInfo> items)182     public synchronized void removeItem(Context context, Iterable<? extends ItemInfo> items) {
183         ArraySet<UserHandle> updatedDeepShortcuts = new ArraySet<>();
184         for (ItemInfo item : items) {
185             switch (item.itemType) {
186                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
187                     folders.remove(item.id);
188                     if (FeatureFlags.IS_STUDIO_BUILD) {
189                         for (ItemInfo info : itemsIdMap) {
190                             if (info.container == item.id) {
191                                 // We are deleting a folder which still contains items that
192                                 // think they are contained by that folder.
193                                 String msg = "deleting a folder (" + item + ") which still " +
194                                         "contains items (" + info + ")";
195                                 Log.e(TAG, msg);
196                             }
197                         }
198                     }
199                     workspaceItems.remove(item);
200                     break;
201                 case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
202                     updatedDeepShortcuts.add(item.user);
203                     // Fall through.
204                 }
205                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
206                 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
207                     workspaceItems.remove(item);
208                     break;
209                 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
210                 case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
211                     appWidgets.remove(item);
212                     break;
213             }
214             itemsIdMap.remove(item.id);
215         }
216         updatedDeepShortcuts.forEach(user -> updateShortcutPinnedState(context, user));
217     }
218 
addItem(Context context, ItemInfo item, boolean newItem)219     public synchronized void addItem(Context context, ItemInfo item, boolean newItem) {
220         addItem(context, item, newItem, null);
221     }
222 
addItem( Context context, ItemInfo item, boolean newItem, @Nullable LoaderMemoryLogger logger)223     public synchronized void addItem(
224             Context context, ItemInfo item, boolean newItem, @Nullable LoaderMemoryLogger logger) {
225         if (logger != null) {
226             logger.addLog(
227                     Log.DEBUG,
228                     TAG,
229                     String.format("Adding item to ID map: %s", item.toString()),
230                     /* stackTrace= */ null);
231         }
232 
233         itemsIdMap.put(item.id, item);
234         switch (item.itemType) {
235             case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
236                 folders.put(item.id, (FolderInfo) item);
237                 workspaceItems.add(item);
238                 break;
239             case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
240             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
241             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
242                 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
243                         item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
244                     workspaceItems.add(item);
245                 } else {
246                     if (newItem) {
247                         if (!folders.containsKey(item.container)) {
248                             // Adding an item to a folder that doesn't exist.
249                             String msg = "adding item: " + item + " to a folder that " +
250                                     " doesn't exist";
251                             Log.e(TAG, msg);
252                         }
253                     } else {
254                         findOrMakeFolder(item.container).add((WorkspaceItemInfo) item, false);
255                     }
256 
257                 }
258                 break;
259             case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
260             case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
261                 appWidgets.add((LauncherAppWidgetInfo) item);
262                 break;
263         }
264         if (newItem && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
265             updateShortcutPinnedState(context, item.user);
266         }
267     }
268 
269     /**
270      * Updates the deep shortucts state in system to match out internal model, pinning any missing
271      * shortcuts and unpinning any extra shortcuts.
272      */
updateShortcutPinnedState(Context context)273     public void updateShortcutPinnedState(Context context) {
274         for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
275             updateShortcutPinnedState(context, user);
276         }
277     }
278 
279     /**
280      * Updates the deep shortucts state in system to match out internal model, pinning any missing
281      * shortcuts and unpinning any extra shortcuts.
282      */
updateShortcutPinnedState(Context context, UserHandle user)283     public synchronized void updateShortcutPinnedState(Context context, UserHandle user) {
284         if (GO_DISABLE_WIDGETS) {
285             return;
286         }
287 
288         // Collect all system shortcuts
289         QueryResult result = new ShortcutRequest(context, user)
290                 .query(PINNED | FLAG_GET_KEY_FIELDS_ONLY);
291         if (!result.wasSuccess()) {
292             return;
293         }
294         // Map of packageName to shortcutIds that are currently in the system
295         Map<String, Set<String>> systemMap = result.stream()
296                 .collect(groupingBy(ShortcutInfo::getPackage,
297                         mapping(ShortcutInfo::getId, Collectors.toSet())));
298 
299         // Collect all model shortcuts
300         Stream.Builder<WorkspaceItemInfo> itemStream = Stream.builder();
301         forAllWorkspaceItemInfos(user, itemStream::accept);
302         // Map of packageName to shortcutIds that are currently in our model
303         Map<String, Set<String>> modelMap = Stream.concat(
304                     // Model shortcuts
305                     itemStream.build()
306                         .filter(wi -> wi.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
307                         .map(ShortcutKey::fromItemInfo),
308                     // Pending shortcuts
309                     ItemInstallQueue.INSTANCE.get(context).getPendingShortcuts(user))
310                 .collect(groupingBy(ShortcutKey::getPackageName,
311                         mapping(ShortcutKey::getId, Collectors.toSet())));
312 
313         // Check for diff
314         for (Map.Entry<String, Set<String>> entry : modelMap.entrySet()) {
315             Set<String> modelShortcuts = entry.getValue();
316             Set<String> systemShortcuts = systemMap.remove(entry.getKey());
317             if (systemShortcuts == null) {
318                 systemShortcuts = Collections.emptySet();
319             }
320 
321             // Do not use .equals as it can vary based on the type of set
322             if (systemShortcuts.size() != modelShortcuts.size()
323                     || !systemShortcuts.containsAll(modelShortcuts)) {
324                 // Update system state for this package
325                 try {
326                     context.getSystemService(LauncherApps.class).pinShortcuts(
327                             entry.getKey(), new ArrayList<>(modelShortcuts), user);
328                 } catch (SecurityException | IllegalStateException e) {
329                     Log.w(TAG, "Failed to pin shortcut", e);
330                 }
331             }
332         }
333 
334         // If there are any extra pinned shortcuts, remove them
335         systemMap.keySet().forEach(packageName -> {
336             // Update system state
337             try {
338                 context.getSystemService(LauncherApps.class).pinShortcuts(
339                         packageName, Collections.emptyList(), user);
340             } catch (SecurityException | IllegalStateException e) {
341                 Log.w(TAG, "Failed to unpin shortcut", e);
342             }
343         });
344     }
345 
346     /**
347      * Return an existing FolderInfo object if we have encountered this ID previously,
348      * or make a new one.
349      */
findOrMakeFolder(int id)350     public synchronized FolderInfo findOrMakeFolder(int id) {
351         // See if a placeholder was created for us already
352         FolderInfo folderInfo = folders.get(id);
353         if (folderInfo == null) {
354             // No placeholder -- create a new instance
355             folderInfo = new FolderInfo();
356             folders.put(id, folderInfo);
357         }
358         return folderInfo;
359     }
360 
361     /**
362      * Clear all the deep shortcut counts for the given package, and re-add the new shortcut counts.
363      */
updateDeepShortcutCounts( String packageName, UserHandle user, List<ShortcutInfo> shortcuts)364     public synchronized void updateDeepShortcutCounts(
365             String packageName, UserHandle user, List<ShortcutInfo> shortcuts) {
366         if (packageName != null) {
367             Iterator<ComponentKey> keysIter = deepShortcutMap.keySet().iterator();
368             while (keysIter.hasNext()) {
369                 ComponentKey next = keysIter.next();
370                 if (next.componentName.getPackageName().equals(packageName)
371                         && next.user.equals(user)) {
372                     keysIter.remove();
373                 }
374             }
375         }
376 
377         // Now add the new shortcuts to the map.
378         for (ShortcutInfo shortcut : shortcuts) {
379             boolean shouldShowInContainer = shortcut.isEnabled()
380                     && (shortcut.isDeclaredInManifest() || shortcut.isDynamic())
381                     && shortcut.getActivity() != null;
382             if (shouldShowInContainer) {
383                 ComponentKey targetComponent
384                         = new ComponentKey(shortcut.getActivity(), shortcut.getUserHandle());
385 
386                 Integer previousCount = deepShortcutMap.get(targetComponent);
387                 deepShortcutMap.put(targetComponent, previousCount == null ? 1 : previousCount + 1);
388             }
389         }
390     }
391 
392     /**
393      * Returns a list containing all workspace items including widgets.
394      */
getAllWorkspaceItems()395     public synchronized ArrayList<ItemInfo> getAllWorkspaceItems() {
396         ArrayList<ItemInfo> items = new ArrayList<>(workspaceItems.size() + appWidgets.size());
397         items.addAll(workspaceItems);
398         items.addAll(appWidgets);
399         return items;
400     }
401 
402     /**
403      * Calls the provided {@code op} for all workspaceItems in the in-memory model (both persisted
404      * items and dynamic/predicted items for the provided {@code userHandle}.
405      * Note the call is not synchronized over the model, that should be handled by the called.
406      */
forAllWorkspaceItemInfos(UserHandle userHandle, Consumer<WorkspaceItemInfo> op)407     public void forAllWorkspaceItemInfos(UserHandle userHandle, Consumer<WorkspaceItemInfo> op) {
408         for (ItemInfo info : itemsIdMap) {
409             if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
410                 op.accept((WorkspaceItemInfo) info);
411             }
412         }
413 
414         for (int i = extraItems.size() - 1; i >= 0; i--) {
415             for (ItemInfo info : extraItems.valueAt(i).items) {
416                 if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
417                     op.accept((WorkspaceItemInfo) info);
418                 }
419             }
420         }
421     }
422 
423     /**
424      * An object containing items corresponding to a fixed container
425      */
426     public static class FixedContainerItems {
427 
428         public final int containerId;
429         public final List<ItemInfo> items;
430 
FixedContainerItems(int containerId)431         public FixedContainerItems(int containerId) {
432             this(containerId, new ArrayList<>());
433         }
434 
FixedContainerItems(int containerId, List<ItemInfo> items)435         public FixedContainerItems(int containerId, List<ItemInfo> items) {
436             this.containerId = containerId;
437             this.items = items;
438         }
439 
440         @Override
clone()441         public FixedContainerItems clone() {
442             return new FixedContainerItems(containerId, new ArrayList<>(items));
443         }
444 
setItems(List<ItemInfo> newItems)445         public void setItems(List<ItemInfo> newItems) {
446             items.clear();
447             newItems.forEach(item -> {
448                 item.container = containerId;
449                 items.add(item);
450             });
451         }
452     }
453 
454 
455     public interface Callbacks {
456         // If the launcher has permission to access deep shortcuts.
457         int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0;
458         // If quiet mode is enabled for any user
459         int FLAG_QUIET_MODE_ENABLED = 1 << 1;
460         // If launcher can change quiet mode
461         int FLAG_QUIET_MODE_CHANGE_PERMISSION = 1 << 2;
462 
463         /**
464          * Returns an IntSet of page ids to bind first, synchronously if possible
465          * or an empty IntSet
466          * @param orderedScreenIds All the page ids to be bound
467          */
getPagesToBindSynchronously(IntArray orderedScreenIds)468         default IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
469             return new IntSet();
470         }
471 
clearPendingBinds()472         default void clearPendingBinds() { }
startBinding()473         default void startBinding() { }
474 
bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons)475         default void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
bindScreens(IntArray orderedScreenIds)476         default void bindScreens(IntArray orderedScreenIds) { }
finishBindingItems(IntSet pagesBoundFirst)477         default void finishBindingItems(IntSet pagesBoundFirst) { }
preAddApps()478         default void preAddApps() { }
bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated)479         default void bindAppsAdded(IntArray newScreens,
480                 ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated) { }
481 
482         /**
483          * Called when some persistent property of an item is modified
484          */
bindItemsModified(List<ItemInfo> items)485         default void bindItemsModified(List<ItemInfo> items) { }
486 
487         /**
488          * Binds updated incremental download progress
489          */
bindIncrementalDownloadProgressUpdated(AppInfo app)490         default void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated)491         default void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets)492         default void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
bindRestoreItemsChange(HashSet<ItemInfo> updates)493         default void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher)494         default void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
bindAllWidgets(List<WidgetsListBaseEntry> widgets)495         default void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
496 
onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks)497         default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
498             pendingTasks.executeAllAndDestroy();
499         }
500 
bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap)501         default void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
502 
503         /**
504          * Binds extra item provided any external source
505          */
bindExtraContainerItems(FixedContainerItems item)506         default void bindExtraContainerItems(FixedContainerItems item) { }
507 
bindAllApplications(AppInfo[] apps, int flags)508         default void bindAllApplications(AppInfo[] apps, int flags) { }
509     }
510 }
511