Files
lawnchair/src/com/android/launcher3/model/BgDataModel.java
Steven Ng 2f5648a911 Refactoring before adding a new view type in the WidgetsListAdapter
Changes made:
1. Model: added an abstract class for storing common information for
   entries shown in the full page widgets picker.
2. Introduced a ViewHolderBinder interface to split the logic of binding
   data to ViewHolder into separate classes.
3. Move the view holder binding of WidgetsListRow from WidgetListAdapter
   to its new class.
4. Move some widgets picker classes into a new picker package.

Test: Auto: Run WidgetsListAdapterTest, WidgetsListRowEntryTest and
      WidgetsListRowViewHolderBinderTest.
      Manual: open the all apps widgets tray and navigate the list.

Bug: 179797520
Change-Id: Iab29557842bb79156cad84d00a4c5d0db0c5aa06
2021-02-10 21:23:40 +00:00

483 lines
19 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 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.ItemInfoMatcher;
import com.android.launcher3.util.ViewOnDrawExecutor;
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.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();
/**
* 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) {
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) {
this(containerId, new ArrayList<>());
}
public FixedContainerItems(int containerId, List<ItemInfo> items) {
this.containerId = containerId;
this.items = items;
}
@Override
public FixedContainerItems clone() {
return new FixedContainerItems(containerId, new ArrayList<>(items));
}
public void setItems(List<ItemInfo> newItems) {
items.clear();
newItems.forEach(item -> {
item.container = containerId;
items.add(item);
});
}
}
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 the page number to bind first, synchronously if possible or -1
*/
int getPageToBindSynchronously();
void clearPendingBinds();
void startBinding();
void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
void bindScreens(IntArray orderedScreenIds);
void finishFirstPageBind(ViewOnDrawExecutor executor);
void finishBindingItems(int pageBoundFirst);
void preAddApps();
void bindAppsAdded(IntArray newScreens,
ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated);
/**
* Binds updated incremental download progress
*/
void bindIncrementalDownloadProgressUpdated(AppInfo app);
void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated);
void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
void bindRestoreItemsChange(HashSet<ItemInfo> updates);
void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
void bindAllWidgets(List<WidgetsListBaseEntry> widgets);
void onPageBoundSynchronously(int page);
void executeOnNextDraw(ViewOnDrawExecutor executor);
void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
/**
* Binds extra item provided any external source
*/
default void bindExtraContainerItems(FixedContainerItems item) { }
void bindAllApplications(AppInfo[] apps, int flags);
}
}