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