Files
lawnchair/src/com/android/launcher3/model/BgDataModel.java
Stefan Andonian a04de842d1 Fix ConcurrentModificationExceptions during binding.
The same lists of extra items stored in BgModelData are also stored in
UI components. This is causing ConcurrentModificationExceptions. The
solution is to clone these lists or mark them as immutable before storing
them in their respective components.

Bug: 206918543
Test: Verified that crash no longer occurs after fix.
Change-Id: I571a2c451af58137aa7513b372b6a8ecf9bd3ff6
2022-11-17 20:46:51 +00:00

505 lines
20 KiB
Java

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY;
import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* All the data stored in-memory and managed by the LauncherModel
*/
public class BgDataModel {
private static final String TAG = "BgDataModel";
/**
* Map of all the ItemInfos (shortcuts, folders, and widgets) created by
* LauncherModel to their ids
*/
public final IntSparseArrayMap<ItemInfo> itemsIdMap = new IntSparseArrayMap<>();
/**
* List of all the folders and shortcuts directly on the home screen (no widgets
* or shortcuts within folders).
*/
public final ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
/**
* All LauncherAppWidgetInfo created by LauncherModel.
*/
public final ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
/**
* Map of id to FolderInfos of all the folders created by LauncherModel
*/
public final IntSparseArrayMap<FolderInfo> folders = new IntSparseArrayMap<>();
/**
* Extra container based items
*/
public final IntSparseArrayMap<FixedContainerItems> extraItems = new IntSparseArrayMap<>();
/**
* Maps all launcher activities to counts of their shortcuts.
*/
public final HashMap<ComponentKey, Integer> deepShortcutMap = new HashMap<>();
/**
* Entire list of widgets.
*/
public final WidgetsModel widgetsModel = new WidgetsModel();
/**
* Cache for strings used in launcher
*/
public final StringCache stringCache = new StringCache();
/**
* Id when the model was last bound
*/
public int lastBindId = 0;
/**
* Clears all the data
*/
public synchronized void clear() {
workspaceItems.clear();
appWidgets.clear();
folders.clear();
itemsIdMap.clear();
deepShortcutMap.clear();
extraItems.clear();
}
/**
* Creates an array of valid workspace screens based on current items in the model.
*/
public synchronized IntArray collectWorkspaceScreens() {
IntSet screenSet = new IntSet();
for (ItemInfo item: itemsIdMap) {
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
screenSet.add(item.screenId);
}
}
if (FeatureFlags.QSB_ON_FIRST_SCREEN || screenSet.isEmpty()) {
screenSet.add(Workspace.FIRST_SCREEN_ID);
}
return screenSet.getArray();
}
public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer,
String[] args) {
writer.println(prefix + "Data Model:");
writer.println(prefix + " ---- workspace items ");
for (int i = 0; i < workspaceItems.size(); i++) {
writer.println(prefix + '\t' + workspaceItems.get(i).toString());
}
writer.println(prefix + " ---- appwidget items ");
for (int i = 0; i < appWidgets.size(); i++) {
writer.println(prefix + '\t' + appWidgets.get(i).toString());
}
writer.println(prefix + " ---- folder items ");
for (int i = 0; i< folders.size(); i++) {
writer.println(prefix + '\t' + folders.valueAt(i).toString());
}
writer.println(prefix + " ---- items id map ");
for (int i = 0; i< itemsIdMap.size(); i++) {
writer.println(prefix + '\t' + itemsIdMap.valueAt(i).toString());
}
if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
writer.println(prefix + "shortcut counts ");
for (Integer count : deepShortcutMap.values()) {
writer.print(count + ", ");
}
writer.println();
}
}
public synchronized void removeItem(Context context, ItemInfo... items) {
removeItem(context, Arrays.asList(items));
}
public synchronized void removeItem(Context context, Iterable<? extends ItemInfo> items) {
ArraySet<UserHandle> updatedDeepShortcuts = new ArraySet<>();
for (ItemInfo item : items) {
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
folders.remove(item.id);
if (FeatureFlags.IS_STUDIO_BUILD) {
for (ItemInfo info : itemsIdMap) {
if (info.container == item.id) {
// We are deleting a folder which still contains items that
// think they are contained by that folder.
String msg = "deleting a folder (" + item + ") which still " +
"contains items (" + info + ")";
Log.e(TAG, msg);
}
}
}
workspaceItems.remove(item);
break;
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
updatedDeepShortcuts.add(item.user);
// Fall through.
}
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
workspaceItems.remove(item);
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
appWidgets.remove(item);
break;
}
itemsIdMap.remove(item.id);
}
updatedDeepShortcuts.forEach(user -> updateShortcutPinnedState(context, user));
}
public synchronized void addItem(Context context, ItemInfo item, boolean newItem) {
addItem(context, item, newItem, null);
}
public synchronized void addItem(
Context context, ItemInfo item, boolean newItem, @Nullable LoaderMemoryLogger logger) {
if (logger != null) {
logger.addLog(
Log.DEBUG,
TAG,
String.format("Adding item to ID map: %s", item.toString()),
/* stackTrace= */ null);
}
itemsIdMap.put(item.id, item);
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
folders.put(item.id, (FolderInfo) item);
workspaceItems.add(item);
break;
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
workspaceItems.add(item);
} else {
if (newItem) {
if (!folders.containsKey(item.container)) {
// Adding an item to a folder that doesn't exist.
String msg = "adding item: " + item + " to a folder that " +
" doesn't exist";
Log.e(TAG, msg);
}
} else {
findOrMakeFolder(item.container).add((WorkspaceItemInfo) item, false);
}
}
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
appWidgets.add((LauncherAppWidgetInfo) item);
break;
}
if (newItem && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
updateShortcutPinnedState(context, item.user);
}
}
/**
* Updates the deep shortucts state in system to match out internal model, pinning any missing
* shortcuts and unpinning any extra shortcuts.
*/
public void updateShortcutPinnedState(Context context) {
for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
updateShortcutPinnedState(context, user);
}
}
/**
* Updates the deep shortucts state in system to match out internal model, pinning any missing
* shortcuts and unpinning any extra shortcuts.
*/
public synchronized void updateShortcutPinnedState(Context context, UserHandle user) {
if (GO_DISABLE_WIDGETS) {
return;
}
// Collect all system shortcuts
QueryResult result = new ShortcutRequest(context, user)
.query(PINNED | FLAG_GET_KEY_FIELDS_ONLY);
if (!result.wasSuccess()) {
return;
}
// Map of packageName to shortcutIds that are currently in the system
Map<String, Set<String>> systemMap = result.stream()
.collect(groupingBy(ShortcutInfo::getPackage,
mapping(ShortcutInfo::getId, Collectors.toSet())));
// Collect all model shortcuts
Stream.Builder<WorkspaceItemInfo> itemStream = Stream.builder();
forAllWorkspaceItemInfos(user, itemStream::accept);
// Map of packageName to shortcutIds that are currently in our model
Map<String, Set<String>> modelMap = Stream.concat(
// Model shortcuts
itemStream.build()
.filter(wi -> wi.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
.map(ShortcutKey::fromItemInfo),
// Pending shortcuts
ItemInstallQueue.INSTANCE.get(context).getPendingShortcuts(user))
.collect(groupingBy(ShortcutKey::getPackageName,
mapping(ShortcutKey::getId, Collectors.toSet())));
// Check for diff
for (Map.Entry<String, Set<String>> entry : modelMap.entrySet()) {
Set<String> modelShortcuts = entry.getValue();
Set<String> systemShortcuts = systemMap.remove(entry.getKey());
if (systemShortcuts == null) {
systemShortcuts = Collections.emptySet();
}
// Do not use .equals as it can vary based on the type of set
if (systemShortcuts.size() != modelShortcuts.size()
|| !systemShortcuts.containsAll(modelShortcuts)) {
// Update system state for this package
try {
context.getSystemService(LauncherApps.class).pinShortcuts(
entry.getKey(), new ArrayList<>(modelShortcuts), user);
} catch (SecurityException | IllegalStateException e) {
Log.w(TAG, "Failed to pin shortcut", e);
}
}
}
// If there are any extra pinned shortcuts, remove them
systemMap.keySet().forEach(packageName -> {
// Update system state
try {
context.getSystemService(LauncherApps.class).pinShortcuts(
packageName, Collections.emptyList(), user);
} catch (SecurityException | IllegalStateException e) {
Log.w(TAG, "Failed to unpin shortcut", e);
}
});
}
/**
* Return an existing FolderInfo object if we have encountered this ID previously,
* or make a new one.
*/
public synchronized FolderInfo findOrMakeFolder(int id) {
// See if a placeholder was created for us already
FolderInfo folderInfo = folders.get(id);
if (folderInfo == null) {
// No placeholder -- create a new instance
folderInfo = new FolderInfo();
folders.put(id, folderInfo);
}
return folderInfo;
}
/**
* Clear all the deep shortcut counts for the given package, and re-add the new shortcut counts.
*/
public synchronized void updateDeepShortcutCounts(
String packageName, UserHandle user, List<ShortcutInfo> shortcuts) {
if (packageName != null) {
Iterator<ComponentKey> keysIter = deepShortcutMap.keySet().iterator();
while (keysIter.hasNext()) {
ComponentKey next = keysIter.next();
if (next.componentName.getPackageName().equals(packageName)
&& next.user.equals(user)) {
keysIter.remove();
}
}
}
// Now add the new shortcuts to the map.
for (ShortcutInfo shortcut : shortcuts) {
boolean shouldShowInContainer = shortcut.isEnabled()
&& (shortcut.isDeclaredInManifest() || shortcut.isDynamic())
&& shortcut.getActivity() != null;
if (shouldShowInContainer) {
ComponentKey targetComponent
= new ComponentKey(shortcut.getActivity(), shortcut.getUserHandle());
Integer previousCount = deepShortcutMap.get(targetComponent);
deepShortcutMap.put(targetComponent, previousCount == null ? 1 : previousCount + 1);
}
}
}
/**
* Returns a list containing all workspace items including widgets.
*/
public synchronized ArrayList<ItemInfo> getAllWorkspaceItems() {
ArrayList<ItemInfo> items = new ArrayList<>(workspaceItems.size() + appWidgets.size());
items.addAll(workspaceItems);
items.addAll(appWidgets);
return items;
}
/**
* Calls the provided {@code op} for all workspaceItems in the in-memory model (both persisted
* items and dynamic/predicted items for the provided {@code userHandle}.
* Note the call is not synchronized over the model, that should be handled by the called.
*/
public void forAllWorkspaceItemInfos(UserHandle userHandle, Consumer<WorkspaceItemInfo> op) {
for (ItemInfo info : itemsIdMap) {
if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
op.accept((WorkspaceItemInfo) info);
}
}
for (int i = extraItems.size() - 1; i >= 0; i--) {
for (ItemInfo info : extraItems.valueAt(i).items) {
if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
op.accept((WorkspaceItemInfo) info);
}
}
}
}
/**
* An object containing items corresponding to a fixed container
*/
public static class FixedContainerItems {
public final int containerId;
public final List<ItemInfo> items;
public FixedContainerItems(int containerId, List<ItemInfo> items) {
this.containerId = containerId;
this.items = Collections.unmodifiableList(items);
}
}
public interface Callbacks {
// If the launcher has permission to access deep shortcuts.
int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0;
// If quiet mode is enabled for any user
int FLAG_QUIET_MODE_ENABLED = 1 << 1;
// If launcher can change quiet mode
int FLAG_QUIET_MODE_CHANGE_PERMISSION = 1 << 2;
/**
* Returns an IntSet of page ids to bind first, synchronously if possible
* or an empty IntSet
* @param orderedScreenIds All the page ids to be bound
*/
@NonNull
default IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
return new IntSet();
}
default void clearPendingBinds() { }
default void startBinding() { }
default void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
default void bindScreens(IntArray orderedScreenIds) { }
default void finishBindingItems(IntSet pagesBoundFirst) { }
default void preAddApps() { }
default void bindAppsAdded(IntArray newScreens,
ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated) { }
/**
* Called when some persistent property of an item is modified
*/
default void bindItemsModified(List<ItemInfo> items) { }
/**
* Binds updated incremental download progress
*/
default void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
default void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
default void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
default void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { }
default void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
pendingTasks.executeAllAndDestroy();
}
default void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
/**
* Binds extra item provided any external source
*/
default void bindExtraContainerItems(FixedContainerItems item) { }
default void bindAllApplications(AppInfo[] apps, int flags) { }
/**
* Binds the cache of string resources
*/
default void bindStringCache(StringCache cache) { }
}
}