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