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.BuildConfig.QSB_ON_FIRST_SCREEN; 21 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; 22 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle; 23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; 24 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; 25 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; 26 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; 27 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER; 28 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET; 29 import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED; 30 31 import static java.util.stream.Collectors.groupingBy; 32 import static java.util.stream.Collectors.mapping; 33 34 import android.content.Context; 35 import android.content.pm.LauncherApps; 36 import android.content.pm.ShortcutInfo; 37 import android.os.UserHandle; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.util.Pair; 41 import android.view.View; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 46 import com.android.launcher3.BuildConfig; 47 import com.android.launcher3.Workspace; 48 import com.android.launcher3.config.FeatureFlags; 49 import com.android.launcher3.dagger.LauncherAppSingleton; 50 import com.android.launcher3.logging.FileLog; 51 import com.android.launcher3.model.data.AppInfo; 52 import com.android.launcher3.model.data.CollectionInfo; 53 import com.android.launcher3.model.data.ItemInfo; 54 import com.android.launcher3.model.data.WorkspaceItemInfo; 55 import com.android.launcher3.pm.UserCache; 56 import com.android.launcher3.shortcuts.ShortcutKey; 57 import com.android.launcher3.shortcuts.ShortcutRequest; 58 import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult; 59 import com.android.launcher3.util.ComponentKey; 60 import com.android.launcher3.util.IntArray; 61 import com.android.launcher3.util.IntSet; 62 import com.android.launcher3.util.IntSparseArrayMap; 63 import com.android.launcher3.util.ItemInflater; 64 import com.android.launcher3.util.PackageUserKey; 65 import com.android.launcher3.util.RunnableList; 66 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 67 68 import java.io.FileDescriptor; 69 import java.io.PrintWriter; 70 import java.util.ArrayList; 71 import java.util.Arrays; 72 import java.util.Collections; 73 import java.util.HashMap; 74 import java.util.Iterator; 75 import java.util.List; 76 import java.util.Map; 77 import java.util.Set; 78 import java.util.function.Consumer; 79 import java.util.function.Predicate; 80 import java.util.stream.Collectors; 81 import java.util.stream.Stream; 82 83 import javax.inject.Inject; 84 85 /** 86 * All the data stored in-memory and managed by the LauncherModel 87 * 88 * All the static data should be accessed on the background thread, A lock should be acquired on 89 * this object when accessing any data from this model. 90 */ 91 @LauncherAppSingleton 92 public class BgDataModel { 93 94 private static final String TAG = "BgDataModel"; 95 96 /** 97 * Map of all the ItemInfos (shortcuts, folders, and widgets) created by 98 * LauncherModel to their ids 99 */ 100 public final IntSparseArrayMap<ItemInfo> itemsIdMap = 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; 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 * Load id for which the callbacks were successfully bound 129 */ 130 public int lastLoadId = -1; 131 public boolean isFirstPagePinnedItemEnabled = QSB_ON_FIRST_SCREEN 132 && !enableSmartspaceRemovalToggle(); 133 134 @Inject BgDataModel(WidgetsModel widgetsModel)135 public BgDataModel(WidgetsModel widgetsModel) { 136 this.widgetsModel = widgetsModel; 137 } 138 139 /** 140 * Clears all the data 141 */ clear()142 public synchronized void clear() { 143 itemsIdMap.clear(); 144 deepShortcutMap.clear(); 145 extraItems.clear(); 146 } 147 148 /** 149 * Creates an array of valid workspace screens based on current items in the model. 150 */ collectWorkspaceScreens()151 public synchronized IntArray collectWorkspaceScreens() { 152 IntSet screenSet = new IntSet(); 153 for (ItemInfo item: itemsIdMap) { 154 if (item.container == CONTAINER_DESKTOP) { 155 screenSet.add(item.screenId); 156 } 157 } 158 if ((FeatureFlags.QSB_ON_FIRST_SCREEN 159 && !SHOULD_SHOW_FIRST_PAGE_WIDGET) 160 || screenSet.isEmpty()) { 161 screenSet.add(Workspace.FIRST_SCREEN_ID); 162 } 163 return screenSet.getArray(); 164 } 165 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)166 public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer, 167 String[] args) { 168 writer.println(prefix + "Data Model:"); 169 writer.println(prefix + " ---- items id map "); 170 for (int i = 0; i < itemsIdMap.size(); i++) { 171 writer.println(prefix + '\t' + itemsIdMap.valueAt(i).toString()); 172 } 173 writer.println(prefix + " ---- extra items "); 174 for (int i = 0; i < extraItems.size(); i++) { 175 writer.println(prefix + '\t' + extraItems.valueAt(i).toString()); 176 } 177 178 if (args.length > 0 && TextUtils.equals(args[0], "--all")) { 179 writer.println(prefix + "shortcut counts "); 180 for (Integer count : deepShortcutMap.values()) { 181 writer.print(count + ", "); 182 } 183 writer.println(); 184 } 185 } 186 removeItem(Context context, ItemInfo... items)187 public synchronized void removeItem(Context context, ItemInfo... items) { 188 removeItem(context, Arrays.asList(items)); 189 } 190 removeItem(Context context, List<? extends ItemInfo> items)191 public synchronized void removeItem(Context context, List<? extends ItemInfo> items) { 192 if (BuildConfig.IS_STUDIO_BUILD) { 193 items.stream() 194 .filter(item -> item.itemType == ITEM_TYPE_FOLDER 195 || item.itemType == ITEM_TYPE_APP_PAIR) 196 .forEach(item -> itemsIdMap.stream() 197 .filter(info -> info.container == item.id) 198 // We are deleting a collection which still contains items that 199 // think they are contained by that collection. 200 .forEach(info -> Log.e(TAG, 201 "deleting a collection (" + item + ") which still contains" 202 + " items (" + info + ")"))); 203 } 204 205 items.forEach(item -> itemsIdMap.remove(item.id)); 206 items.stream().map(info -> info.user).distinct().forEach( 207 user -> updateShortcutPinnedState(context, user)); 208 } 209 addItem(Context context, ItemInfo item, boolean newItem)210 public synchronized void addItem(Context context, ItemInfo item, boolean newItem) { 211 itemsIdMap.put(item.id, item); 212 if (newItem && item.itemType == ITEM_TYPE_DEEP_SHORTCUT) { 213 updateShortcutPinnedState(context, item.user); 214 } 215 if (BuildConfig.IS_DEBUG_DEVICE 216 && newItem 217 && item.container != CONTAINER_DESKTOP 218 && item.container != CONTAINER_HOTSEAT 219 && !(itemsIdMap.get(item.container) instanceof CollectionInfo)) { 220 // Adding an item to a nonexistent collection. 221 Log.e(TAG, "attempted to add item: " + item + " to a nonexistent app collection"); 222 } 223 } 224 225 /** 226 * Updates the deep shortcuts state in system to match out internal model, pinning any missing 227 * shortcuts and unpinning any extra shortcuts. 228 */ updateShortcutPinnedState(Context context)229 public void updateShortcutPinnedState(Context context) { 230 for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) { 231 updateShortcutPinnedState(context, user); 232 } 233 } 234 235 /** 236 * Updates the deep shortucts state in system to match out internal model, pinning any missing 237 * shortcuts and unpinning any extra shortcuts. 238 */ updateShortcutPinnedState(Context context, UserHandle user)239 public synchronized void updateShortcutPinnedState(Context context, UserHandle user) { 240 if (!WIDGETS_ENABLED) { 241 return; 242 } 243 244 // Collect all system shortcuts 245 QueryResult result = new ShortcutRequest(context, user) 246 .query(PINNED | FLAG_GET_KEY_FIELDS_ONLY); 247 if (!result.wasSuccess()) { 248 return; 249 } 250 // Map of packageName to shortcutIds that are currently in the system 251 Map<String, Set<String>> systemMap = result.stream() 252 .collect(groupingBy(ShortcutInfo::getPackage, 253 mapping(ShortcutInfo::getId, Collectors.toSet()))); 254 255 // Collect all model shortcuts 256 Stream.Builder<WorkspaceItemInfo> itemStream = Stream.builder(); 257 forAllWorkspaceItemInfos(user, itemStream::accept); 258 // Map of packageName to shortcutIds that are currently in our model 259 Map<String, Set<String>> modelMap = Stream.concat( 260 // Model shortcuts 261 itemStream.build() 262 .filter(wi -> wi.itemType == ITEM_TYPE_DEEP_SHORTCUT) 263 .map(ShortcutKey::fromItemInfo), 264 // Pending shortcuts 265 ItemInstallQueue.INSTANCE.get(context).getPendingShortcuts(user)) 266 .collect(groupingBy(ShortcutKey::getPackageName, 267 mapping(ShortcutKey::getId, Collectors.toSet()))); 268 269 // Check for diff 270 for (Map.Entry<String, Set<String>> entry : modelMap.entrySet()) { 271 Set<String> modelShortcuts = entry.getValue(); 272 Set<String> systemShortcuts = systemMap.remove(entry.getKey()); 273 if (systemShortcuts == null) { 274 systemShortcuts = Collections.emptySet(); 275 } 276 277 // Do not use .equals as it can vary based on the type of set 278 if (systemShortcuts.size() != modelShortcuts.size() 279 || !systemShortcuts.containsAll(modelShortcuts)) { 280 // Update system state for this package 281 try { 282 FileLog.d(TAG, "updateShortcutPinnedState:" 283 + " Pinning Shortcuts: " + entry.getKey() + ": " + modelShortcuts); 284 context.getSystemService(LauncherApps.class).pinShortcuts( 285 entry.getKey(), new ArrayList<>(modelShortcuts), user); 286 } catch (SecurityException | IllegalStateException e) { 287 Log.w(TAG, "Failed to pin shortcut", e); 288 } 289 } 290 } 291 292 // If there are any extra pinned shortcuts, remove them 293 systemMap.keySet().forEach(packageName -> { 294 // Update system state 295 try { 296 FileLog.d(TAG, "updateShortcutPinnedState:" 297 + " Unpinning extra Shortcuts for package: " + packageName 298 + ": " + systemMap.get(packageName)); 299 context.getSystemService(LauncherApps.class).pinShortcuts( 300 packageName, Collections.emptyList(), user); 301 } catch (SecurityException | IllegalStateException e) { 302 Log.w(TAG, "Failed to unpin shortcut", e); 303 } 304 }); 305 } 306 307 /** 308 * Clear all the deep shortcut counts for the given package, and re-add the new shortcut counts. 309 */ updateDeepShortcutCounts( String packageName, UserHandle user, List<ShortcutInfo> shortcuts)310 public synchronized void updateDeepShortcutCounts( 311 String packageName, UserHandle user, List<ShortcutInfo> shortcuts) { 312 if (packageName != null) { 313 Iterator<ComponentKey> keysIter = deepShortcutMap.keySet().iterator(); 314 while (keysIter.hasNext()) { 315 ComponentKey next = keysIter.next(); 316 if (next.componentName.getPackageName().equals(packageName) 317 && next.user.equals(user)) { 318 keysIter.remove(); 319 } 320 } 321 } 322 323 // Now add the new shortcuts to the map. 324 for (ShortcutInfo shortcut : shortcuts) { 325 boolean shouldShowInContainer = shortcut.isEnabled() 326 && (shortcut.isDeclaredInManifest() || shortcut.isDynamic()) 327 && shortcut.getActivity() != null; 328 if (shouldShowInContainer) { 329 ComponentKey targetComponent 330 = new ComponentKey(shortcut.getActivity(), shortcut.getUserHandle()); 331 332 Integer previousCount = deepShortcutMap.get(targetComponent); 333 deepShortcutMap.put(targetComponent, previousCount == null ? 1 : previousCount + 1); 334 } 335 } 336 } 337 338 /** 339 * Calls the provided {@code op} for all workspaceItems in the in-memory model (both persisted 340 * items and dynamic/predicted items for the provided {@code userHandle}. 341 * Note the call is not synchronized over the model, that should be handled by the called. 342 */ forAllWorkspaceItemInfos(UserHandle userHandle, Consumer<WorkspaceItemInfo> op)343 public void forAllWorkspaceItemInfos(UserHandle userHandle, Consumer<WorkspaceItemInfo> op) { 344 for (ItemInfo info : itemsIdMap) { 345 if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) { 346 op.accept((WorkspaceItemInfo) info); 347 } 348 } 349 350 for (int i = extraItems.size() - 1; i >= 0; i--) { 351 for (ItemInfo info : extraItems.valueAt(i).items) { 352 if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) { 353 op.accept((WorkspaceItemInfo) info); 354 } 355 } 356 } 357 } 358 359 /** 360 * An object containing items corresponding to a fixed container 361 */ 362 public static class FixedContainerItems { 363 364 public final int containerId; 365 public final List<ItemInfo> items; 366 FixedContainerItems(int containerId, List<ItemInfo> items)367 public FixedContainerItems(int containerId, List<ItemInfo> items) { 368 this.containerId = containerId; 369 this.items = Collections.unmodifiableList(items); 370 } 371 372 @Override 373 @NonNull toString()374 public final String toString() { 375 StringBuilder s = new StringBuilder(); 376 s.append("FixedContainerItems:"); 377 s.append(" id=").append(containerId); 378 s.append(" itemCount=").append(items.size()); 379 for (int i = 0; i < items.size(); i++) { 380 s.append(" item #").append(i).append(": ").append(items.get(i).toString()); 381 } 382 return s.toString(); 383 } 384 385 } 386 387 388 public interface Callbacks { 389 // If the launcher has permission to access deep shortcuts. 390 int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0; 391 // If quiet mode is enabled for any user 392 int FLAG_QUIET_MODE_ENABLED = 1 << 1; 393 // If launcher can change quiet mode 394 int FLAG_QUIET_MODE_CHANGE_PERMISSION = 1 << 2; 395 // If quiet mode is enabled for work profile user 396 int FLAG_WORK_PROFILE_QUIET_MODE_ENABLED = 1 << 3; 397 // If quiet mode is enabled for private profile user 398 int FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED = 1 << 4; 399 400 /** 401 * Returns an IntSet of page ids to bind first, synchronously if possible 402 * or an empty IntSet 403 * @param orderedScreenIds All the page ids to be bound 404 */ 405 @NonNull getPagesToBindSynchronously(IntArray orderedScreenIds)406 default IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) { 407 return new IntSet(); 408 } 409 clearPendingBinds()410 default void clearPendingBinds() { } startBinding()411 default void startBinding() { } 412 413 @Nullable getItemInflater()414 default ItemInflater getItemInflater() { 415 return null; 416 } 417 bindItems(@onNull List<ItemInfo> shortcuts, boolean forceAnimateIcons)418 default void bindItems(@NonNull List<ItemInfo> shortcuts, boolean forceAnimateIcons) { } 419 /** Alternate method to bind preinflated views */ bindInflatedItems(@onNull List<Pair<ItemInfo, View>> items)420 default void bindInflatedItems(@NonNull List<Pair<ItemInfo, View>> items) { } 421 bindScreens(IntArray orderedScreenIds)422 default void bindScreens(IntArray orderedScreenIds) { } setIsFirstPagePinnedItemEnabled(boolean isFirstPagePinnedItemEnabled)423 default void setIsFirstPagePinnedItemEnabled(boolean isFirstPagePinnedItemEnabled) { } finishBindingItems(IntSet pagesBoundFirst)424 default void finishBindingItems(IntSet pagesBoundFirst) { } preAddApps()425 default void preAddApps() { } bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated)426 default void bindAppsAdded(IntArray newScreens, 427 ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated) { } 428 429 /** 430 * Called when some persistent property of an item is modified 431 */ bindItemsModified(List<ItemInfo> items)432 default void bindItemsModified(List<ItemInfo> items) { } 433 434 /** 435 * Binds updated incremental download progress 436 */ bindIncrementalDownloadProgressUpdated(AppInfo app)437 default void bindIncrementalDownloadProgressUpdated(AppInfo app) { } 438 439 /** Called when a runtime property of the ItemInfo is updated due to some system event */ bindItemsUpdated(Set<ItemInfo> updates)440 default void bindItemsUpdated(Set<ItemInfo> updates) { } bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher)441 default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { } 442 443 /** 444 * Binds the app widgets to the providers that share widgets with the UI. 445 */ bindAllWidgets(@onNull List<WidgetsListBaseEntry> widgets)446 default void bindAllWidgets(@NonNull List<WidgetsListBaseEntry> widgets) { } 447 bindSmartspaceWidget()448 default void bindSmartspaceWidget() { } 449 450 /** Called when workspace has been bound. */ onInitialBindComplete(@onNull IntSet boundPages, @NonNull RunnableList pendingTasks, @NonNull RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync)451 default void onInitialBindComplete(@NonNull IntSet boundPages, 452 @NonNull RunnableList pendingTasks, 453 @NonNull RunnableList onCompleteSignal, 454 int workspaceItemCount, boolean isBindSync) { 455 pendingTasks.executeAllAndDestroy(); 456 } 457 bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap)458 default void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { } 459 460 /** 461 * Binds extra item provided any external source 462 */ bindExtraContainerItems(FixedContainerItems item)463 default void bindExtraContainerItems(FixedContainerItems item) { } 464 bindAllApplications(AppInfo[] apps, int flags, Map<PackageUserKey, Integer> packageUserKeytoUidMap)465 default void bindAllApplications(AppInfo[] apps, int flags, 466 Map<PackageUserKey, Integer> packageUserKeytoUidMap) { 467 } 468 469 /** 470 * Binds the cache of string resources 471 */ bindStringCache(StringCache cache)472 default void bindStringCache(StringCache cache) { } 473 } 474 } 475