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