From 742630c2f80c7ab128174665c1dd2eb041cba244 Mon Sep 17 00:00:00 2001 From: Jeremy Sim Date: Fri, 22 Mar 2024 22:11:18 -0700 Subject: [PATCH] Reparent folders and app pairs Previously, app pairs and folders shared a common data model, FolderInfo. Now we need to separate them, so a new type, CollectionInfo, will serve as the parent of both types. Bug: 315731527 Fixes: 326664798 Flag: ACONFIG com.android.wm.shell.enable_app_pairs TRUNKFOOD Test: Manual, unit tests to follow Change-Id: Ia8c429cf6e6a376f2554ae1866549ef0bcab2a22 --- .../model/QuickstepModelDelegate.java | 15 ++-- .../taskbar/TaskbarActivityContext.java | 9 ++- .../taskbar/TaskbarPopupController.java | 4 +- .../launcher3/taskbar/TaskbarView.java | 18 +++-- .../taskbar/TaskbarViewController.java | 4 +- .../logging/StatsLogCompatManager.java | 9 ++- .../quickstep/util/AppPairsController.java | 22 ++---- .../util/SplitAnimationController.kt | 4 +- .../util/SplitToWorkspaceController.java | 5 +- src/com/android/launcher3/BubbleTextView.java | 3 +- .../android/launcher3/DeleteDropTarget.java | 4 +- src/com/android/launcher3/Launcher.java | 28 ++++--- src/com/android/launcher3/Workspace.java | 12 ++- .../BaseAccessibilityDelegate.java | 5 +- .../LauncherAccessibilityDelegate.java | 16 ++-- .../WorkspaceAccessibilityHelper.java | 2 +- .../launcher3/apppairs/AppPairIcon.java | 52 +++---------- .../launcher3/apppairs/AppPairIconGraphic.kt | 34 +++++---- .../launcher3/dragndrop/DragController.java | 4 +- src/com/android/launcher3/folder/Folder.java | 14 ++-- .../launcher3/folder/FolderGridOrganizer.java | 2 +- .../android/launcher3/folder/FolderIcon.java | 10 +-- .../launcher3/folder/FolderNameProvider.java | 12 +-- .../launcher3/folder/LauncherDelegate.java | 2 +- .../graphics/LauncherPreviewRenderer.java | 14 ++-- .../model/AddWorkspaceItemsTask.java | 6 +- .../android/launcher3/model/BgDataModel.java | 42 ++++++----- .../launcher3/model/FirstScreenBroadcast.java | 29 +++---- .../android/launcher3/model/LoaderTask.java | 75 +++++++++++++------ .../launcher3/model/ModelDbController.java | 66 ++++++++++++++++ .../android/launcher3/model/ModelWriter.java | 16 ++-- .../launcher3/model/PackageUpdatedTask.java | 9 +-- .../launcher3/model/WorkspaceItemProcessor.kt | 47 ++++++++---- .../launcher3/model/data/AppPairInfo.kt | 65 ++++++++++++++++ .../launcher3/model/data/CollectionInfo.kt | 48 ++++++++++++ .../launcher3/model/data/FolderInfo.java | 57 +++----------- .../launcher3/model/data/ItemInfo.java | 13 ++-- .../model/data/LauncherAppWidgetInfo.java | 4 +- .../launcher3/touch/ItemClickHandler.java | 15 ++-- .../android/launcher3/util/ItemInflater.kt | 3 +- .../launcher3/util/ItemInfoMatcher.java | 11 +-- .../util/LauncherBindableItemsContainer.java | 5 +- .../widget/PendingAddWidgetInfo.java | 6 +- .../celllayout/FavoriteItemsTransaction.java | 7 +- .../folder/PreviewItemManagerTest.kt | 5 +- .../model/CacheDataUpdatedTaskTest.java | 2 +- .../model/DefaultLayoutProviderTest.java | 8 +- .../launcher3/model/FolderIconLoadTest.kt | 8 +- .../android/launcher3/model/LoaderTaskTest.kt | 2 +- .../launcher3/util/ItemInflaterTest.kt | 5 +- 50 files changed, 517 insertions(+), 341 deletions(-) create mode 100644 src/com/android/launcher3/model/data/AppPairInfo.kt create mode 100644 src/com/android/launcher3/model/data/CollectionInfo.kt diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 0ce1cb835c..c0e0587fac 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -18,8 +18,8 @@ package com.android.launcher3.model; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.formatElapsedTime; -import static com.android.launcher3.LauncherPrefs.nonRestorableItem; import static com.android.launcher3.EncryptionType.ENCRYPTED; +import static com.android.launcher3.LauncherPrefs.nonRestorableItem; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; @@ -65,7 +65,7 @@ import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.UserCache; @@ -233,7 +233,7 @@ public class QuickstepModelDelegate extends ModelDelegate { } InstanceId instanceId = new InstanceIdSequence().newInstanceId(); for (ItemInfo info : itemsIdMap) { - FolderInfo parent = getContainer(info, itemsIdMap); + CollectionInfo parent = getContainer(info, itemsIdMap); StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId); } additionalSnapshotEvents(instanceId); @@ -270,7 +270,7 @@ public class QuickstepModelDelegate extends ModelDelegate { } for (ItemInfo info : itemsIdMap) { - FolderInfo parent = getContainer(info, itemsIdMap); + CollectionInfo parent = getContainer(info, itemsIdMap); LauncherAtom.ItemInfo itemInfo = info.buildProto(parent); Log.d(TAG, itemInfo.toString()); StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo, @@ -293,18 +293,19 @@ public class QuickstepModelDelegate extends ModelDelegate { } } - private static FolderInfo getContainer(ItemInfo info, IntSparseArrayMap itemsIdMap) { + private static CollectionInfo getContainer( + ItemInfo info, IntSparseArrayMap itemsIdMap) { if (info.container > 0) { ItemInfo containerInfo = itemsIdMap.get(info.container); - if (!(containerInfo instanceof FolderInfo)) { + if (!(containerInfo instanceof CollectionInfo)) { Log.e(TAG, String.format( "Item info: %s found with invalid container: %s", info, containerInfo)); } // Allow crash to help debug b/173838775 - return (FolderInfo) containerInfo; + return (CollectionInfo) containerInfo; } return null; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 26212c1934..a3aa93adfd 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -89,6 +89,7 @@ import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -1082,19 +1083,19 @@ public class TaskbarActivityContext extends BaseTaskbarContext { ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key, ActivityOptions.makeBasic()); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); - } else if (tag instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_FOLDER) { + } else if (tag instanceof FolderInfo) { // Tapping an expandable folder icon on Taskbar shouldCloseAllOpenViews = false; expandFolder((FolderIcon) view); - } else if (tag instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR) { + } else if (tag instanceof AppPairInfo api) { // Tapping an app pair icon on Taskbar if (recents != null && recents.isSplitSelectionActive()) { Toast.makeText(this, "Unable to split with an app pair. Select another app.", Toast.LENGTH_SHORT).show(); } else { // Else launch the selected app pair - launchFromTaskbar(recents, view, fi.contents); - mControllers.uiController.onTaskbarIconLaunched(fi); + launchFromTaskbar(recents, view, api.getContents()); + mControllers.uiController.onTaskbarIconLaunched(api); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } } else if (tag instanceof WorkspaceItemInfo) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java index ca192c8cbb..4462f20810 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -116,9 +116,9 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba } } else if (info instanceof FolderInfo && v instanceof FolderIcon) { FolderInfo fi = (FolderInfo) info; - if (fi.contents.stream().anyMatch(matcher)) { + if (fi.anyMatch(matcher)) { FolderDotInfo folderDotInfo = new FolderDotInfo(); - for (WorkspaceItemInfo si : fi.contents) { + for (WorkspaceItemInfo si : fi.getContents()) { folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si)); } ((FolderIcon) v).setDotInfo(folderDotInfo); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index c81bf7aacc..67b41c6438 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -18,6 +18,7 @@ package com.android.launcher3.taskbar; import static android.content.pm.PackageManager.FEATURE_PC; import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; +import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR; import static com.android.launcher3.Flags.enableCursorHoverStates; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER; @@ -52,6 +53,8 @@ import com.android.launcher3.Utilities; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.folder.PreviewBackground; +import com.android.launcher3.model.data.AppPairInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -282,7 +285,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar removeView(view); view.setOnClickListener(null); view.setOnLongClickListener(null); - if (!(view.getTag() instanceof FolderInfo)) { + if (!(view.getTag() instanceof CollectionInfo)) { mActivityContext.getViewCache().recycleView(view.getSourceLayoutResId(), view); } view.setTag(null); @@ -316,8 +319,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar boolean isCollection = false; if (hotseatItemInfo.isPredictedItem()) { expectedLayoutResId = R.layout.taskbar_predicted_app_icon; - } else if (hotseatItemInfo instanceof FolderInfo fi) { - expectedLayoutResId = fi.itemType == ITEM_TYPE_APP_PAIR + } else if (hotseatItemInfo instanceof CollectionInfo ci) { + expectedLayoutResId = ci.itemType == ITEM_TYPE_APP_PAIR ? R.layout.app_pair_icon : R.layout.folder_icon; isCollection = true; @@ -345,17 +348,18 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (hotseatView == null) { if (isCollection) { - FolderInfo folderInfo = (FolderInfo) hotseatItemInfo; + CollectionInfo collectionInfo = (CollectionInfo) hotseatItemInfo; switch (hotseatItemInfo.itemType) { case ITEM_TYPE_FOLDER: hotseatView = FolderIcon.inflateFolderAndIcon( - expectedLayoutResId, mActivityContext, this, folderInfo); + expectedLayoutResId, mActivityContext, this, + (FolderInfo) collectionInfo); ((FolderIcon) hotseatView).setTextVisible(false); break; case ITEM_TYPE_APP_PAIR: hotseatView = AppPairIcon.inflateIcon( - expectedLayoutResId, mActivityContext, this, folderInfo, - BubbleTextView.DISPLAY_TASKBAR); + expectedLayoutResId, mActivityContext, this, + (AppPairInfo) collectionInfo, DISPLAY_TASKBAR); ((AppPairIcon) hotseatView).setTextVisible(false); break; default: diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 4b1963b2ba..1f7f0a7569 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -813,8 +813,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar * 3) All Apps button */ public View getFirstIconMatch(Predicate matcher) { - Predicate folderMatcher = ItemInfoMatcher.forFolderMatch(matcher); - return mTaskbarView.getFirstMatch(matcher, folderMatcher); + Predicate collectionMatcher = ItemInfoMatcher.forFolderMatch(matcher); + return mTaskbarView.getFirstMatch(matcher, collectionMatcher); } /** diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java index d265918e18..e078a497c9 100644 --- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java +++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java @@ -61,7 +61,7 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.AllAppsList; import com.android.launcher3.model.BaseModelUpdateTask; import com.android.launcher3.model.BgDataModel; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.Executors; import com.android.launcher3.util.LogConfig; @@ -375,7 +375,7 @@ public class StatsLogCompatManager extends StatsLogManager { Executors.MODEL_EXECUTOR.execute( () -> write(event, applyOverwrites(mItemInfo.buildProto()))); } else { - // Item is inside the folder, fetch folder info in a BG thread + // Item is inside a collection, fetch collection info in a BG thread // and then write to StatsLog. appState.getModel().enqueueModelUpdateTask( new BaseModelUpdateTask() { @@ -383,8 +383,9 @@ public class StatsLogCompatManager extends StatsLogManager { public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) { - FolderInfo folderInfo = dataModel.folders.get(mItemInfo.container); - write(event, applyOverwrites(mItemInfo.buildProto(folderInfo))); + CollectionInfo collectionInfo = + dataModel.collections.get(mItemInfo.container); + write(event, applyOverwrites(mItemInfo.buildProto(collectionInfo))); } }); } diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java index ecb6118e46..f4da867646 100644 --- a/quickstep/src/com/android/quickstep/util/AppPairsController.java +++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java @@ -53,7 +53,7 @@ import com.android.launcher3.icons.IconCache; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.taskbar.TaskbarActivityContext; @@ -149,25 +149,17 @@ public class AppPairsController { app1.rank = encodeRank(SPLIT_POSITION_TOP_OR_LEFT, snapPosition); app2.rank = encodeRank(SPLIT_POSITION_BOTTOM_OR_RIGHT, snapPosition); - FolderInfo newAppPair = FolderInfo.createAppPair(app1, app2); - - if (newAppPair.contents.size() != 2) { - // if app pair doesn't have exactly 2 members, log an error and do not create the app - // pair. - Log.wtf(TAG, - "tried to save an app pair with " + newAppPair.contents.size() + " members"); - return; - } + AppPairInfo newAppPair = new AppPairInfo(app1, app2); IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache(); MODEL_EXECUTOR.execute(() -> { - newAppPair.contents.forEach(member -> { + newAppPair.getContents().forEach(member -> { member.title = ""; member.bitmap = iconCache.getDefaultIcon(newAppPair.user); iconCache.getTitleAndIcon(member, member.usingLowResIcon()); }); - newAppPair.title = getDefaultTitle(newAppPair.contents.get(0).title, - newAppPair.contents.get(1).title); + newAppPair.title = getDefaultTitle(newAppPair.getFirstApp().title, + newAppPair.getSecondApp().title); MAIN_EXECUTOR.execute(() -> { LauncherAccessibilityDelegate delegate = Launcher.getLauncher(mContext).getAccessibilityDelegate(); @@ -194,8 +186,8 @@ public class AppPairsController { * monitoring */ public void launchAppPair(AppPairIcon appPairIcon, int cuj) { - WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0); - WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1); + WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp(); + WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp(); ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user); ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user); mSplitSelectStateController.setLaunchingCuj(cuj); diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 8f5c9c1186..6b27004593 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -659,8 +659,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // Create a new floating view in Launcher, positioned above the launching icon val drawableArea = launchingIconView.iconDrawableArea - val appIcon1 = launchingIconView.info.contents[0].newIcon(launchingIconView.context) - val appIcon2 = launchingIconView.info.contents[1].newIcon(launchingIconView.context) + val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context) + val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context) appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) val floatingView = diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java index 87be091e4c..2282c46dfd 100644 --- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java +++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java @@ -16,7 +16,6 @@ package com.android.quickstep.util; -import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.window.flags.Flags.enableDesktopWindowingMode; @@ -44,7 +43,7 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.IconCache; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -126,7 +125,7 @@ public class SplitToWorkspaceController { intent = appInfo.intent; user = appInfo.user; bitmapInfo = appInfo.bitmap; - } else if (tag instanceof FolderInfo fi && fi.itemType == ITEM_TYPE_APP_PAIR) { + } else if (tag instanceof AppPairInfo) { // Prompt the user to select something else by wiggling the instructions view mController.getSplitInstructionsView().goBoing(); return true; diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 1285aca8b2..4369fc3bea 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -435,8 +435,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } @UiThread - @VisibleForTesting - public void applyLabel(ItemInfoWithIcon info) { + public void applyLabel(ItemInfo info) { CharSequence label = info.title; if (label != null) { mLastOriginalText = label; diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 9a5627a28c..58789fd8f2 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -27,7 +27,7 @@ import android.view.View; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.logging.StatsLogManager; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -75,7 +75,7 @@ public class DeleteDropTarget extends ButtonDropTarget { } return (info instanceof LauncherAppWidgetInfo) - || (info instanceof FolderInfo); + || (info instanceof CollectionInfo); } @Override diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index e7d2843b64..0d4dbebebb 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -202,6 +202,8 @@ import com.android.launcher3.model.ItemInstallQueue; import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.AppPairInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -798,13 +800,19 @@ public class Launcher extends StatefulActivity @Override public void invalidateParent(ItemInfo info) { if (info.container >= 0) { - View folderIcon = getWorkspace().getHomescreenIconByItemId(info.container); - if (folderIcon instanceof FolderIcon && folderIcon.getTag() instanceof FolderInfo) { + View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container); + if (collectionIcon instanceof FolderIcon folderIcon + && collectionIcon.getTag() instanceof FolderInfo) { if (new FolderGridOrganizer(getDeviceProfile()) .setFolderInfo((FolderInfo) folderIcon.getTag()) .isItemInPreview(info.rank)) { folderIcon.invalidate(); } + } else if (collectionIcon instanceof AppPairIcon appPairIcon + && collectionIcon.getTag() instanceof AppPairInfo appPairInfo) { + if (appPairInfo.getContents().contains(info)) { + appPairIcon.getIconDrawableArea().redraw(); + } } } } @@ -2003,24 +2011,26 @@ public class Launcher extends StatefulActivity public boolean removeItem(View v, final ItemInfo itemInfo, boolean deleteFromDb, @Nullable final String reason) { if (itemInfo instanceof WorkspaceItemInfo) { - // Remove the shortcut from the folder before removing it from launcher - View folderIcon = mWorkspace.getHomescreenIconByItemId(itemInfo.container); - if (folderIcon instanceof FolderIcon) { - ((FolderInfo) folderIcon.getTag()).remove((WorkspaceItemInfo) itemInfo, true); + View collectionIcon = mWorkspace.getHomescreenIconByItemId(itemInfo.container); + if (collectionIcon instanceof FolderIcon) { + // Remove the shortcut from the folder before removing it from launcher + ((FolderInfo) collectionIcon.getTag()).remove((WorkspaceItemInfo) itemInfo, true); + } else if (collectionIcon instanceof AppPairIcon appPairIcon) { + removeItem(appPairIcon, appPairIcon.getInfo(), deleteFromDb, + "removing app pair because one of its member apps was removed"); } else { mWorkspace.removeWorkspaceItem(v); } if (deleteFromDb) { getModelWriter().deleteItemFromDatabase(itemInfo, reason); } - } else if (itemInfo instanceof FolderInfo) { - final FolderInfo folderInfo = (FolderInfo) itemInfo; + } else if (itemInfo instanceof CollectionInfo ci) { if (v instanceof FolderIcon) { ((FolderIcon) v).removeListeners(); } mWorkspace.removeWorkspaceItem(v); if (deleteFromDb) { - getModelWriter().deleteFolderAndContentsFromDatabase(folderInfo); + getModelWriter().deleteCollectionAndContentsFromDatabase(ci); } } else if (itemInfo instanceof LauncherAppWidgetInfo) { final LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) itemInfo; diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index ca34dd1a0a..ce3c55adb7 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -95,6 +95,7 @@ import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.LauncherEvent; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -3313,7 +3314,7 @@ public class Workspace extends PagedView } } else if (child instanceof FolderIcon) { FolderInfo folderInfo = (FolderInfo) info; - List matches = folderInfo.contents.stream() + List matches = folderInfo.getContents().stream() .filter(matcher) .collect(Collectors.toList()); if (!matches.isEmpty()) { @@ -3322,6 +3323,11 @@ public class Workspace extends PagedView ((FolderIcon) child).getFolder().close(false /* animate */); } } + } else if (info instanceof AppPairInfo api) { + // If an app pair's member apps are being removed, delete the whole app pair. + if (api.anyMatch(matcher)) { + mLauncher.removeItem(child, info, true); + } } } } @@ -3373,9 +3379,9 @@ public class Workspace extends PagedView } } else if (info instanceof FolderInfo && v instanceof FolderIcon) { FolderInfo fi = (FolderInfo) info; - if (fi.contents.stream().anyMatch(matcher)) { + if (fi.anyMatch(matcher)) { FolderDotInfo folderDotInfo = new FolderDotInfo(); - for (WorkspaceItemInfo si : fi.contents) { + for (WorkspaceItemInfo si : fi.getContents()) { folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si)); } ((FolderIcon) v).setDotInfo(folderDotInfo); diff --git a/src/com/android/launcher3/accessibility/BaseAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/BaseAccessibilityDelegate.java index 19d042147b..29862aee8d 100644 --- a/src/com/android/launcher3/accessibility/BaseAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/BaseAccessibilityDelegate.java @@ -28,7 +28,7 @@ import com.android.launcher3.DropTarget; import com.android.launcher3.LauncherSettings; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -45,6 +45,7 @@ public abstract class BaseAccessibilityDelegate workspace = mContext.getWorkspace(); workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); - mContext.getModelWriter().addItemToDatabase(fi, + mContext.getModelWriter().addItemToDatabase(ci, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); - fi.contents.forEach(member -> { - mContext.getModelWriter().addItemToDatabase(member, fi.id, -1, -1, -1); - }); - bindItem(fi, accessibility, finishCallback); + ci.getContents().forEach(member -> + mContext.getModelWriter() + .addItemToDatabase(member, ci.id, -1, -1, -1)); + bindItem(ci, accessibility, finishCallback); } })); return true; diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java index a8624dd17d..52073cc77a 100644 --- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -149,7 +149,7 @@ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelega // Find the first item in the folder. FolderInfo folder = (FolderInfo) info; WorkspaceItemInfo firstItem = null; - for (WorkspaceItemInfo shortcut : folder.contents) { + for (WorkspaceItemInfo shortcut : folder.getContents()) { if (firstItem == null || firstItem.rank > shortcut.rank) { firstItem = shortcut; } diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java index bbeb34199f..9010f8215d 100644 --- a/src/com/android/launcher3/apppairs/AppPairIcon.java +++ b/src/com/android/launcher3/apppairs/AppPairIcon.java @@ -33,7 +33,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.Reorderable; import com.android.launcher3.dragndrop.DraggableView; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.views.ActivityContext; @@ -50,17 +50,12 @@ import java.util.function.Predicate; public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable { private static final String TAG = "AppPairIcon"; - /** - * Indicates that the app pair is currently launchable on the current screen. - */ - private boolean mIsLaunchableAtScreenSize = true; - // A view that holds the app pair icon graphic. private AppPairIconGraphic mIconGraphic; // A view that holds the app pair's title. private BubbleTextView mAppPairName; // The underlying ItemInfo that stores info about the app pair members, etc. - private FolderInfo mInfo; + private AppPairInfo mInfo; // The containing element that holds this icon: workspace, taskbar, folder, etc. Affects certain // aspects of how the icon is drawn. private int mContainer; @@ -81,7 +76,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab * Builds an AppPairIcon to be added to the Launcher. */ public static AppPairIcon inflateIcon(int resId, ActivityContext activity, - @Nullable ViewGroup group, FolderInfo appPairInfo, int container) { + @Nullable ViewGroup group, AppPairInfo appPairInfo, int container) { DeviceProfile grid = activity.getDeviceProfile(); LayoutInflater inflater = (group != null) ? LayoutInflater.from(group.getContext()) @@ -89,7 +84,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false); // Sort contents, so that left-hand app comes first - appPairInfo.contents.sort(Comparator.comparingInt(a -> a.rank)); + appPairInfo.getContents().sort(Comparator.comparingInt(a -> a.rank)); icon.setTag(appPairInfo); icon.setOnClickListener(activity.getItemOnClickListener()); @@ -100,8 +95,6 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic); icon.mIconGraphic.init(icon, container); - icon.checkDisabledState(); - // Set up app pair title icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name); FrameLayout.LayoutParams lp = @@ -115,7 +108,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab // For some reason, app icons have setIncludeFontPadding(false) inside folders, so we set it // here to match that. icon.mAppPairName.setIncludeFontPadding(container != DISPLAY_FOLDER); - icon.mAppPairName.setText(appPairInfo.title); + icon.mAppPairName.applyLabel(appPairInfo); // Set up accessibility icon.setContentDescription(icon.getAccessibilityTitle(appPairInfo)); @@ -127,9 +120,9 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab /** * Returns a formatted accessibility title for app pairs. */ - public String getAccessibilityTitle(FolderInfo appPairInfo) { - CharSequence app1 = appPairInfo.contents.get(0).title; - CharSequence app2 = appPairInfo.contents.get(1).title; + public String getAccessibilityTitle(AppPairInfo appPairInfo) { + CharSequence app1 = appPairInfo.getFirstApp().title; + CharSequence app2 = appPairInfo.getSecondApp().title; return getContext().getString(R.string.app_pair_name_format, app1, app2); } @@ -174,7 +167,7 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab return mScaleForReorderBounce; } - public FolderInfo getInfo() { + public AppPairInfo getInfo() { return mInfo; } @@ -186,41 +179,20 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab return mIconGraphic; } - public boolean isLaunchableAtScreenSize() { - return mIsLaunchableAtScreenSize; - } - - /** - * Updates the "disabled" state of the app pair in the current device configuration. - * App pairs can be "disabled" in two ways: - * 1) One of the member WorkspaceItemInfos is disabled (i.e. the app software itself is paused - * by the user or can't be launched for some other reason). - * 2) This specific instance of an app pair can't be launched due to screen size requirements. - */ - public void checkDisabledState() { - DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile(); - // If user is on a small screen, we can't launch if either of the apps is non-resizeable - mIsLaunchableAtScreenSize = - dp.isTablet || getInfo().contents.stream().noneMatch( - wii -> wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE)); - // Invalidate to update icons - mIconGraphic.redraw(); - } - /** * Called when WorkspaceItemInfos get updated, and the app pair icon may need to be redrawn. */ public void maybeRedrawForWorkspaceUpdate(Predicate itemCheck) { // If either of the app pair icons return true on the predicate (i.e. in the list of // updated apps), redraw the icon graphic (icon background and both icons). - if (getInfo().contents.stream().anyMatch(itemCheck)) { - checkDisabledState(); + if (getInfo().anyMatch(itemCheck)) { + mIconGraphic.redraw(); } } /** * Inside folders, icons are vertically centered in their rows. See - * {@link BubbleTextView#onMeasure(int, int)} for comparison. + * {@link BubbleTextView} for comparison. */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt index 04050b0294..a3a1cfccde 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt @@ -23,12 +23,12 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.Gravity import android.widget.FrameLayout +import androidx.annotation.OpenForTesting import com.android.launcher3.DeviceProfile import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener import com.android.launcher3.icons.BitmapInfo import com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter -import com.android.launcher3.model.data.FolderInfo -import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.model.data.AppPairInfo import com.android.launcher3.util.Themes import com.android.launcher3.views.ActivityContext @@ -36,29 +36,32 @@ import com.android.launcher3.views.ActivityContext * A FrameLayout marking the area on an [AppPairIcon] where the visual icon will be drawn. One of * two child UI elements on an [AppPairIcon], along with a BubbleTextView holding the text title. */ -class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : +@OpenForTesting +open class AppPairIconGraphic +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs), OnDeviceProfileChangeListener { private val TAG = "AppPairIconGraphic" companion object { - /** Composes a drawable for this icon, consisting of a background and 2 app icons. */ + /** + * Composes a drawable for this icon, consisting of a background and 2 app icons. The app + * pair will draw as "disabled" if either of the following is true: + * 1) One of the member WorkspaceItemInfos is disabled (i.e. the app software itself is + * paused or can't be launched for some other reason). + * 2) One of the member apps can't be launched due to screen size requirements. + */ @JvmStatic - fun composeDrawable(appPairInfo: FolderInfo, p: AppPairIconDrawingParams): Drawable { + fun composeDrawable(appPairInfo: AppPairInfo, p: AppPairIconDrawingParams): Drawable { // Generate new icons, using themed flag if needed. val flags = if (Themes.isThemedIconEnabled(p.context)) BitmapInfo.FLAG_THEMED else 0 - val appIcon1 = appPairInfo.contents[0].newIcon(p.context, flags) - val appIcon2 = appPairInfo.contents[1].newIcon(p.context, flags) + val appIcon1 = appPairInfo.getFirstApp().newIcon(p.context, flags) + val appIcon2 = appPairInfo.getSecondApp().newIcon(p.context, flags) appIcon1.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt()) appIcon2.setBounds(0, 0, p.memberIconSize.toInt(), p.memberIconSize.toInt()) - // Check disabled status. - val activity: ActivityContext = ActivityContext.lookupContext(p.context) - val isLaunchableAtScreenSize = - activity.deviceProfile.isTablet || - appPairInfo.contents.stream().noneMatch { wii: WorkspaceItemInfo -> - wii.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE) - } - val shouldDrawAsDisabled = appPairInfo.isDisabled || !isLaunchableAtScreenSize + val shouldDrawAsDisabled = + appPairInfo.isDisabled || !appPairInfo.isLaunchable(p.context) // Set disabled status on icons. appIcon1.setIsDisabled(shouldDrawAsDisabled) @@ -124,7 +127,6 @@ class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: Attr */ fun getIconBounds(outBounds: Rect) { outBounds.set(0, 0, drawParams.backgroundSize.toInt(), drawParams.backgroundSize.toInt()) - outBounds.offset( // x-coordinate in parent's coordinate system ((parentIcon.width - drawParams.backgroundSize) / 2).toInt(), diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java index b6e5977b87..bc5a164da8 100644 --- a/src/com/android/launcher3/dragndrop/DragController.java +++ b/src/com/android/launcher3/dragndrop/DragController.java @@ -33,6 +33,7 @@ import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; import com.android.launcher3.Flags; import com.android.launcher3.logging.InstanceId; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -289,7 +290,8 @@ public abstract class DragController // Cancel the current drag if we are removing an app that we are dragging if (mDragObject != null) { ItemInfo dragInfo = mDragObject.dragInfo; - if (dragInfo instanceof WorkspaceItemInfo && matcher.test(dragInfo)) { + if ((dragInfo instanceof WorkspaceItemInfo && matcher.test(dragInfo)) + || (dragInfo instanceof AppPairInfo api && api.anyMatch(matcher))) { cancelDrag(); } } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index c8c634a741..aa3c5ba824 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -493,7 +493,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mInfo = info; mFromTitle = info.title; mFromLabelState = info.getFromLabelState(); - ArrayList children = info.contents; + ArrayList children = info.getContents(); Collections.sort(children, ITEM_POS_COMPARATOR); updateItemLocationsInDatabaseBatch(true); @@ -626,7 +626,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // onDropComplete. Perform cleanup once drag-n-drop ends. mDragController.addDragListener(this); - ArrayList items = new ArrayList<>(mInfo.contents); + ArrayList items = new ArrayList<>(mInfo.getContents()); mEmptyCellRank = items.size(); items.add(null); // Add an empty spot at the end @@ -639,7 +639,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo * is played. */ public void animateOpen() { - animateOpen(mInfo.contents, 0); + animateOpen(mInfo.getContents(), 0); } /** @@ -1097,9 +1097,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mActivityContext.getDeviceProfile()).setFolderInfo(mInfo); ArrayList items = new ArrayList<>(); - int total = mInfo.contents.size(); + int total = mInfo.getContents().size(); for (int i = 0; i < total; i++) { - WorkspaceItemInfo itemInfo = mInfo.contents.get(i); + WorkspaceItemInfo itemInfo = mInfo.getContents().get(i); if (verifier.updateRankAndPos(itemInfo, i)) { items.add(itemInfo); } @@ -1113,7 +1113,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo FolderNameInfos nameInfos = new FolderNameInfos(); FolderNameProvider fnp = FolderNameProvider.newInstance(getContext()); fnp.getSuggestedFolderName( - getContext(), mInfo.contents, nameInfos); + getContext(), mInfo.getContents(), nameInfos); mInfo.suggestedFolderNames = nameInfos; }); } @@ -1217,7 +1217,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } public int getItemCount() { - return mInfo.contents.size(); + return mInfo.getContents().size(); } void replaceFolderWithFinalItem() { diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java index cc247619e4..593673d4bd 100644 --- a/src/com/android/launcher3/folder/FolderGridOrganizer.java +++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java @@ -57,7 +57,7 @@ public class FolderGridOrganizer { * Updates the organizer with the provided folder info */ public FolderGridOrganizer setFolderInfo(FolderInfo info) { - return setContentSize(info.contents.size()); + return setContentSize(info.getContents().size()); } /** diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index ee0d5fce24..62ce311c3b 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -215,7 +215,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel // Keep the notification dot up to date with the sum of all the content's dots. FolderDotInfo folderDotInfo = new FolderDotInfo(); - for (WorkspaceItemInfo si : folderInfo.contents) { + for (WorkspaceItemInfo si : folderInfo.getContents()) { folderDotInfo.addDotInfo(activity.getDotInfoForItem(si)); } icon.setDotInfo(folderDotInfo); @@ -422,7 +422,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel FolderNameInfos nameInfos = new FolderNameInfos(); Executors.MODEL_EXECUTOR.post(() -> { d.folderNameProvider.getSuggestedFolderName( - getContext(), mInfo.contents, nameInfos); + getContext(), mInfo.getContents(), nameInfos); postDelayed(() -> { setLabelSuggestion(nameInfos, d.logInstanceId); invalidate(); @@ -487,7 +487,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } mFolder.notifyDrop(); onDrop(item, d, null, 1.0f, - itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(), + itemReturnedOnFailedDrop ? item.rank : mInfo.getContents().size(), itemReturnedOnFailedDrop ); } @@ -666,7 +666,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel * Returns the list of items which should be visible in the preview */ public List getPreviewItemsOnPage(int page) { - return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents); + return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.getContents()); } @Override @@ -809,7 +809,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel * Returns a formatted accessibility title for folder */ public String getAccessiblityTitle(CharSequence title) { - int size = mInfo.contents.size(); + int size = mInfo.getContents().size(); if (size < MAX_NUM_ITEMS_IN_PREVIEW) { return getContext().getString(R.string.folder_name_format_exact, title, size); } else { diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java index bf5959442d..5d2bb3a890 100644 --- a/src/com/android/launcher3/folder/FolderNameProvider.java +++ b/src/com/android/launcher3/folder/FolderNameProvider.java @@ -35,7 +35,7 @@ import com.android.launcher3.model.BaseModelUpdateTask; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.IntSparseArrayMap; import com.android.launcher3.util.Preconditions; @@ -62,7 +62,7 @@ public class FolderNameProvider implements ResourceBasedOverride { * name edit box can also be used to provide suggestion. */ public static final int SUGGEST_MAX = 4; - protected IntSparseArrayMap mFolderInfos; + protected IntSparseArrayMap mCollectionInfos; protected List mAppInfos; /** @@ -79,7 +79,7 @@ public class FolderNameProvider implements ResourceBasedOverride { } public static FolderNameProvider newInstance(Context context, List appInfos, - IntSparseArrayMap folderInfos) { + IntSparseArrayMap folderInfos) { Preconditions.assertWorkerThread(); FolderNameProvider fnp = Overrides.getObject(FolderNameProvider.class, context.getApplicationContext(), R.string.folder_name_provider_class); @@ -93,9 +93,9 @@ public class FolderNameProvider implements ResourceBasedOverride { new FolderNameWorker()); } - private void load(List appInfos, IntSparseArrayMap folderInfos) { + private void load(List appInfos, IntSparseArrayMap folderInfos) { mAppInfos = appInfos; - mFolderInfos = folderInfos; + mCollectionInfos = folderInfos; } /** @@ -195,7 +195,7 @@ public class FolderNameProvider implements ResourceBasedOverride { @Override public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) { - mFolderInfos = dataModel.folders.clone(); + mCollectionInfos = dataModel.collections.clone(); mAppInfos = Arrays.asList(apps.copyData()); } } diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java index 78298b3a14..33bcf217de 100644 --- a/src/com/android/launcher3/folder/LauncherDelegate.java +++ b/src/com/android/launcher3/folder/LauncherDelegate.java @@ -93,7 +93,7 @@ public class LauncherDelegate { // folder CellLayout cellLayout = mLauncher.getCellLayout(info.container, mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId); - finalItem = info.contents.remove(0); + finalItem = info.getContents().remove(0); newIcon = mLauncher.getItemInflater().inflateItem( finalItem, mLauncher.getModelWriter(), cellLayout); mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem, diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index 9aee379d0d..6b3bb5140b 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -82,6 +82,8 @@ import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.model.data.AppPairInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -388,16 +390,16 @@ public class LauncherPreviewRenderer extends ContextWrapper addInScreenFromBind(icon, info); } - private void inflateAndAddCollectionIcon(FolderInfo info) { + private void inflateAndAddCollectionIcon(CollectionInfo info) { boolean isOnDesktop = info.container == Favorites.CONTAINER_DESKTOP; CellLayout screen = isOnDesktop ? mWorkspaceScreens.get(info.screenId) : mHotseat; - FrameLayout folderIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER - ? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, info) - : AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info, + FrameLayout collectionIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER + ? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, (FolderInfo) info) + : AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, (AppPairInfo) info, isOnDesktop ? DISPLAY_WORKSPACE : DISPLAY_TASKBAR); - addInScreenFromBind(folderIcon, info); + addInScreenFromBind(collectionIcon, info); } private void inflateAndAddWidgets( @@ -501,7 +503,7 @@ public class LauncherPreviewRenderer extends ContextWrapper break; case Favorites.ITEM_TYPE_FOLDER: case Favorites.ITEM_TYPE_APP_PAIR: - inflateAndAddCollectionIcon((FolderInfo) itemInfo); + inflateAndAddCollectionIcon((CollectionInfo) itemInfo); break; default: break; diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java index 96a8da97f3..ce563b7184 100644 --- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java +++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java @@ -31,7 +31,7 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -131,8 +131,8 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { int screenId = coords[0]; ItemInfo itemInfo; - if (item instanceof WorkspaceItemInfo || item instanceof FolderInfo || - item instanceof LauncherAppWidgetInfo) { + if (item instanceof WorkspaceItemInfo || item instanceof CollectionInfo + || item instanceof LauncherAppWidgetInfo) { itemInfo = item; } else if (item instanceof WorkspaceItemFactory) { itemInfo = ((WorkspaceItemFactory) item).makeWorkspaceItem(app.getContext()); diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index 8579d1d682..ee9ce7db87 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -44,6 +44,7 @@ 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.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -102,9 +103,9 @@ public class BgDataModel { public final ArrayList appWidgets = new ArrayList<>(); /** - * Map of id to FolderInfos of all the folders created by LauncherModel + * Map of id to CollectionInfos of all the folders or app pairs created by LauncherModel */ - public final IntSparseArrayMap folders = new IntSparseArrayMap<>(); + public final IntSparseArrayMap collections = new IntSparseArrayMap<>(); /** * Extra container based items @@ -144,7 +145,7 @@ public class BgDataModel { public synchronized void clear() { workspaceItems.clear(); appWidgets.clear(); - folders.clear(); + collections.clear(); itemsIdMap.clear(); deepShortcutMap.clear(); extraItems.clear(); @@ -179,9 +180,9 @@ public class BgDataModel { 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 + " ---- collection items "); + for (int i = 0; i < collections.size(); i++) { + writer.println(prefix + '\t' + collections.valueAt(i).toString()); } writer.println(prefix + " ---- extra items "); for (int i = 0; i < extraItems.size(); i++) { @@ -211,12 +212,12 @@ public class BgDataModel { switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: - folders.remove(item.id); + collections.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. + // We are deleting a collection which still contains items that + // think they are contained by that collection. String msg = "deleting a collection (" + item + ") which still " + "contains items (" + info + ")"; Log.e(TAG, msg); @@ -259,7 +260,7 @@ public class BgDataModel { switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: - folders.put(item.id, (FolderInfo) item); + collections.put(item.id, (CollectionInfo) item); workspaceItems.add(item); break; case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: @@ -269,14 +270,14 @@ public class BgDataModel { workspaceItems.add(item); } else { if (newItem) { - if (!folders.containsKey(item.container)) { + if (!collections.containsKey(item.container)) { // Adding an item to a nonexistent collection. String msg = "attempted to add item: " + item + " to a nonexistent app" + " collection"; Log.e(TAG, msg); } } else { - findOrMakeFolder(item.container).add((WorkspaceItemInfo) item, false); + findOrMakeFolder(item.container).add((WorkspaceItemInfo) item); } } break; @@ -371,15 +372,18 @@ public class BgDataModel { * Return an existing FolderInfo object if we have encountered this ID previously, * or make a new one. */ - public synchronized FolderInfo findOrMakeFolder(int id) { + public synchronized CollectionInfo 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); + CollectionInfo collectionInfo = collections.get(id); + if (collectionInfo == null) { + // No placeholder -- create a new blank folder instance. At this point, we don't know + // if the desired container is supposed to be a folder or an app pair. In the case that + // it is an app pair, the blank folder will be replaced by a blank app pair when the app + // pair is getting processed, in WorkspaceItemProcessor.processFolderOrAppPair(). + collectionInfo = new FolderInfo(); + collections.put(id, collectionInfo); } - return folderInfo; + return collectionInfo; } /** diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java index 9e91b9d6e8..1deb6657e1 100644 --- a/src/com/android/launcher3/model/FirstScreenBroadcast.java +++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java @@ -36,6 +36,7 @@ import androidx.annotation.AnyThread; import androidx.annotation.WorkerThread; import com.android.launcher3.LauncherSettings; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -67,7 +68,8 @@ public class FirstScreenBroadcast { private static final String ACTION_FIRST_SCREEN_ACTIVE_INSTALLS = "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS"; - private static final String FOLDER_ITEM_EXTRA = "folderItem"; + // String retained as "folderItem" for back-compatibility reasons. + private static final String COLLECTION_ITEM_EXTRA = "folderItem"; private static final String WORKSPACE_ITEM_EXTRA = "workspaceItem"; private static final String HOTSEAT_ITEM_EXTRA = "hotseatItem"; private static final String WIDGET_ITEM_EXTRA = "widgetItem"; @@ -105,20 +107,19 @@ public class FirstScreenBroadcast { @WorkerThread private void sendBroadcastToInstaller(Context context, String installerPackageName, Set packages, List firstScreenItems) { - Set folderItems = new HashSet<>(); + Set collectionItems = new HashSet<>(); Set workspaceItems = new HashSet<>(); Set hotseatItems = new HashSet<>(); Set widgetItems = new HashSet<>(); for (ItemInfo info : firstScreenItems) { - if (info instanceof FolderInfo) { - FolderInfo folderInfo = (FolderInfo) info; - String folderItemInfoPackage; - for (ItemInfo folderItemInfo : cloneOnMainThread(folderInfo.contents)) { - folderItemInfoPackage = getPackageName(folderItemInfo); - if (folderItemInfoPackage != null - && packages.contains(folderItemInfoPackage)) { - folderItems.add(folderItemInfoPackage); + if (info instanceof CollectionInfo ci) { + String collectionItemInfoPackage; + for (ItemInfo collectionItemInfo : cloneOnMainThread(ci.getContents())) { + collectionItemInfoPackage = getPackageName(collectionItemInfo); + if (collectionItemInfoPackage != null + && packages.contains(collectionItemInfoPackage)) { + collectionItems.add(collectionItemInfoPackage); } } } @@ -137,13 +138,13 @@ public class FirstScreenBroadcast { } if (DEBUG) { - printList(installerPackageName, "Folder item", folderItems); + printList(installerPackageName, "Collection item", collectionItems); printList(installerPackageName, "Workspace item", workspaceItems); printList(installerPackageName, "Hotseat item", hotseatItems); printList(installerPackageName, "Widget item", widgetItems); } - if (folderItems.isEmpty() + if (collectionItems.isEmpty() && workspaceItems.isEmpty() && hotseatItems.isEmpty() && widgetItems.isEmpty()) { @@ -152,7 +153,7 @@ public class FirstScreenBroadcast { } context.sendBroadcast(new Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS) .setPackage(installerPackageName) - .putStringArrayListExtra(FOLDER_ITEM_EXTRA, new ArrayList<>(folderItems)) + .putStringArrayListExtra(COLLECTION_ITEM_EXTRA, new ArrayList<>(collectionItems)) .putStringArrayListExtra(WORKSPACE_ITEM_EXTRA, new ArrayList<>(workspaceItems)) .putStringArrayListExtra(HOTSEAT_ITEM_EXTRA, new ArrayList<>(hotseatItems)) .putStringArrayListExtra(WIDGET_ITEM_EXTRA, new ArrayList<>(widgetItems)) @@ -180,7 +181,7 @@ public class FirstScreenBroadcast { } /** - * Clone the provided list on UI thread. This is used for {@link FolderInfo#contents} which + * Clone the provided list on UI thread. This is used for {@link FolderInfo#getContents()} which * is always modified on UI thread. */ @AnyThread diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 17cef900ec..1971b1656d 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -20,7 +20,6 @@ import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN; import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed; import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE; import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE; -import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL; import static com.android.launcher3.config.FeatureFlags.SMARTSPACE_AS_A_WIDGET; @@ -77,9 +76,12 @@ import com.android.launcher3.icons.ShortcutCachingLogic; import com.android.launcher3.icons.cache.IconCacheUpdateHandler; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.AppPairInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.InstallSessionHelper; @@ -99,7 +101,6 @@ import com.android.launcher3.util.TraceHelper; import com.android.launcher3.widget.WidgetInflater; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -234,6 +235,7 @@ public class LoaderTask implements Runnable { if (Objects.equals(mApp.getInvariantDeviceProfile().dbFile, mDbName)) { verifyNotStopped(); sanitizeFolders(mItemsDeleted); + sanitizeAppPairs(); sanitizeWidgetsShortcutsAndPackages(); logASplit("sanitizeData"); } @@ -482,14 +484,20 @@ public class LoaderTask implements Runnable { } /** - * After all items have been processed and added to the BgDataModel, this method requests - * high-res icons for the items that are part of an app pair + * After all items have been processed and added to the BgDataModel, this method sorts and + * requests high-res icons for the items that are part of an app pair. */ private void processAppPairItems() { - mBgDataModel.workspaceItems.stream() - .filter((itemInfo -> itemInfo.itemType == ITEM_TYPE_APP_PAIR)) - .forEach(fi -> ((FolderInfo) fi).contents.forEach(item -> - mIconCache.getTitleAndIcon(item, false /*useLowResIcon*/))); + for (CollectionInfo collection : mBgDataModel.collections) { + if (!(collection instanceof AppPairInfo appPair)) { + continue; + } + + appPair.getContents().sort(Folder.ITEM_POS_COMPARATOR); + // Fetch hi-res icons if needed. + appPair.getContents().stream().filter(ItemInfoWithIcon::usingLowResIcon) + .forEach(member -> mIconCache.getTitleAndIcon(member, false)); + } } /** @@ -545,20 +553,21 @@ public class LoaderTask implements Runnable { // Sort the folder items, update ranks, and make sure all preview items are high res. List verifiers = mApp.getInvariantDeviceProfile().supportedProfiles .stream().map(FolderGridOrganizer::new).toList(); - for (FolderInfo folder : mBgDataModel.folders) { - Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR); + for (CollectionInfo collection : mBgDataModel.collections) { + if (!(collection instanceof FolderInfo folder)) { + continue; + } + + folder.getContents().sort(Folder.ITEM_POS_COMPARATOR); verifiers.forEach(verifier -> verifier.setFolderInfo(folder)); - int size = folder.contents.size(); + int size = folder.getContents().size(); // Update ranks here to ensure there are no gaps caused by removed folder items. // Ranks are the source of truth for folder items, so cellX and cellY can be // ignored for now. Database will be updated once user manually modifies folder. for (int rank = 0; rank < size; ++rank) { - WorkspaceItemInfo info = folder.contents.get(rank); - // rank is used differently in app pairs, so don't reset - if (folder.itemType != ITEM_TYPE_APP_PAIR) { - info.rank = rank; - } + WorkspaceItemInfo info = folder.getContents().get(rank); + info.rank = rank; if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION && verifiers.stream().anyMatch(it -> it.isItemInPreview(info.rank))) { @@ -611,14 +620,32 @@ public class LoaderTask implements Runnable { IntArray deletedFolderIds = mApp.getModel().getModelDbController().deleteEmptyFolders(); synchronized (mBgDataModel) { for (int folderId : deletedFolderIds) { - mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId)); - mBgDataModel.folders.remove(folderId); + mBgDataModel.workspaceItems.remove(mBgDataModel.collections.get(folderId)); + mBgDataModel.collections.remove(folderId); mBgDataModel.itemsIdMap.remove(folderId); } } } } + /** Cleans up app pairs if they don't have the right number of member apps (2). */ + private void sanitizeAppPairs() { + IntArray deletedAppPairIds = mApp.getModel().getModelDbController().deleteBadAppPairs(); + IntArray deletedAppIds = mApp.getModel().getModelDbController().deleteUnparentedApps(); + + IntArray deleted = new IntArray(); + deleted.addAll(deletedAppPairIds); + deleted.addAll(deletedAppIds); + + synchronized (mBgDataModel) { + for (int id : deleted) { + mBgDataModel.workspaceItems.remove(mBgDataModel.collections.get(id)); + mBgDataModel.collections.remove(id); + mBgDataModel.itemsIdMap.remove(id); + } + } + } + private void sanitizeWidgetsShortcutsAndPackages() { Context context = mApp.getContext(); @@ -754,16 +781,16 @@ public class LoaderTask implements Runnable { private void loadFolderNames() { FolderNameProvider provider = FolderNameProvider.newInstance(mApp.getContext(), - mBgAllAppsList.data, mBgDataModel.folders); + mBgAllAppsList.data, mBgDataModel.collections); synchronized (mBgDataModel) { - for (int i = 0; i < mBgDataModel.folders.size(); i++) { + for (int i = 0; i < mBgDataModel.collections.size(); i++) { FolderNameInfos suggestionInfos = new FolderNameInfos(); - FolderInfo info = mBgDataModel.folders.valueAt(i); - if (info.suggestedFolderNames == null) { - provider.getSuggestedFolderName(mApp.getContext(), info.contents, + CollectionInfo info = mBgDataModel.collections.valueAt(i); + if (info instanceof FolderInfo fi && fi.suggestedFolderNames == null) { + provider.getSuggestedFolderName(mApp.getContext(), fi.getContents(), suggestionInfos); - info.suggestedFolderNames = suggestionInfos; + fi.suggestedFolderNames = suggestionInfos; } } } diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java index 8ed554a12e..7e1d40d96b 100644 --- a/src/com/android/launcher3/model/ModelDbController.java +++ b/src/com/android/launcher3/model/ModelDbController.java @@ -15,11 +15,15 @@ */ package com.android.launcher3.model; +import static android.provider.BaseColumns._ID; import static android.util.Base64.NO_PADDING; import static android.util.Base64.NO_WRAP; import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; +import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb; import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY; import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL; @@ -391,6 +395,68 @@ public class ModelDbController { } } + /** + * Deletes any app pair that doesn't contain 2 member apps from the DB. + * @return Ids of deleted app pairs. + */ + @WorkerThread + public IntArray deleteBadAppPairs() { + createDbIfNotExists(); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + try (SQLiteTransaction t = new SQLiteTransaction(db)) { + // Select all entries with ITEM_TYPE = ITEM_TYPE_APP_PAIR whose id does not appear + // exactly twice in the CONTAINER column. + String selection = + ITEM_TYPE + " = " + ITEM_TYPE_APP_PAIR + + " AND " + _ID + " NOT IN" + + " (SELECT " + CONTAINER + " FROM " + TABLE_NAME + + " GROUP BY " + CONTAINER + " HAVING COUNT(*) = 2)"; + + IntArray appPairIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME, + _ID, selection, null, null); + if (!appPairIds.isEmpty()) { + db.delete(TABLE_NAME, Utilities.createDbSelectionQuery( + _ID, appPairIds), null); + } + t.commit(); + return appPairIds; + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + return new IntArray(); + } + } + + /** + * Deletes any app with a container id that doesn't exist. + * @return Ids of deleted apps. + */ + @WorkerThread + public IntArray deleteUnparentedApps() { + createDbIfNotExists(); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + try (SQLiteTransaction t = new SQLiteTransaction(db)) { + // Select all entries whose container id does not appear in the database. + String selection = + CONTAINER + " >= 0" + + " AND " + CONTAINER + " NOT IN" + + " (SELECT " + _ID + " FROM " + TABLE_NAME + ")"; + + IntArray appIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME, + _ID, selection, null, null); + if (!appIds.isEmpty()) { + db.delete(TABLE_NAME, Utilities.createDbSelectionQuery( + _ID, appIds), null); + } + t.commit(); + return appIds; + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + return new IntArray(); + } + } + private static void addModifiedTime(ContentValues values) { values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis()); } diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java index 55093a3afe..b477cb1e06 100644 --- a/src/com/android/launcher3/model/ModelWriter.java +++ b/src/com/android/launcher3/model/ModelWriter.java @@ -37,7 +37,7 @@ import com.android.launcher3.celllayout.CellPosMapper.CellPos; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.BgDataModel.Callbacks; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -275,7 +275,7 @@ public class ModelWriter { public void deleteItemsFromDatabase(@NonNull final Predicate matcher, @Nullable final String reason) { deleteItemsFromDatabase(StreamSupport.stream(mBgDataModel.itemsIdMap.spliterator(), false) - .filter(matcher).collect(Collectors.toList()), reason); + .filter(matcher).collect(Collectors.toList()), reason); } /** @@ -302,15 +302,15 @@ public class ModelWriter { /** * Remove the specified folder and all its contents from the database. */ - public void deleteFolderAndContentsFromDatabase(final FolderInfo info) { + public void deleteCollectionAndContentsFromDatabase(final CollectionInfo info) { ModelVerifier verifier = new ModelVerifier(); notifyDelete(Collections.singleton(info)); enqueueDeleteRunnable(newModelTask(() -> { mModel.getModelDbController().delete(Favorites.TABLE_NAME, Favorites.CONTAINER + "=" + info.id, null); - mBgDataModel.removeItem(mContext, info.contents); - info.contents.clear(); + mBgDataModel.removeItem(mContext, info.getContents()); + info.getContents().clear(); mModel.getModelDbController().delete(Favorites.TABLE_NAME, Favorites._ID + "=" + info.id, null); @@ -458,12 +458,12 @@ public class ModelWriter { if (item.container != Favorites.CONTAINER_DESKTOP && item.container != Favorites.CONTAINER_HOTSEAT) { - // Item is in a folder, make sure this folder exists - if (!mBgDataModel.folders.containsKey(item.container)) { + // Item is in a collection, make sure this collection exists + if (!mBgDataModel.collections.containsKey(item.container)) { // An items container is being set to a that of an item which is not in // the list of Folders. String msg = "item: " + item + " container being set to: " + - item.container + ", not in the list of folders"; + item.container + ", not in the list of collections"; Log.e(TAG, msg); } } diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 0ba468d5f0..ea1ae2ed24 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -361,17 +361,10 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { } if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { - // This predicate is used to mark an ItemInfo for removal if its package or component - // is marked for removal. - Predicate removeAppMatch = + Predicate removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser) .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); - // This predicate is used to mark an app pair for removal if it contains an app marked - // for removal. - Predicate removeAppPairMatch = - ItemInfoMatcher.forAppPairMatch(removeAppMatch); - Predicate removeMatch = removeAppMatch.or(removeAppPairMatch); deleteAndBindComponentsRemoved(removeMatch, "removed because the corresponding package or component is removed. " + "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect( diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt index 22e5eb4949..aa2929096e 100644 --- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt +++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt @@ -32,6 +32,8 @@ import com.android.launcher3.LauncherSettings.Favorites import com.android.launcher3.Utilities import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError import com.android.launcher3.logging.FileLog +import com.android.launcher3.model.data.AppPairInfo +import com.android.launcher3.model.data.FolderInfo import com.android.launcher3.model.data.IconRequestInfo import com.android.launcher3.model.data.ItemInfoWithIcon import com.android.launcher3.model.data.LauncherAppWidgetInfo @@ -360,25 +362,40 @@ class WorkspaceItemProcessor( } /** - * Loads the folder information from the database and formats it into a FolderInfo. Some of the - * processing for folder content items is done in LoaderTask after all the items in the - * workspace have been loaded. The loaded FolderInfos are stored in the BgDataModel. + * Loads CollectionInfo information from the database and formats it. This function runs while + * LoaderTask is still active; some of the processing for folder content items is done after all + * the items in the workspace have been loaded. The loaded and formatted CollectionInfo is then + * stored in the BgDataModel. */ private fun processFolderOrAppPair() { - val folderInfo = - bgDataModel.findOrMakeFolder(c.id).apply { - c.applyCommonProperties(this) - itemType = c.itemType - // Do not trim the folder label, as is was set by the user. - title = c.getString(c.mTitleIndex) - spanX = 1 - spanY = 1 - options = c.options - } + var collection = bgDataModel.findOrMakeFolder(c.id) + // If we generated a placeholder Folder before this point, it may need to be replaced with + // an app pair. + if (c.itemType == Favorites.ITEM_TYPE_APP_PAIR && collection is FolderInfo) { + val folderInfo: FolderInfo = collection + val newAppPair = AppPairInfo() + // Move the placeholder's contents over to the new app pair. + folderInfo.contents.forEach(newAppPair::add) + collection = newAppPair + // Remove the placeholder and add the app pair into the data model. + bgDataModel.collections.remove(c.id) + bgDataModel.collections.put(c.id, collection) + } + + c.applyCommonProperties(collection) + // Do not trim the folder label, as is was set by the user. + collection.title = c.getString(c.mTitleIndex) + collection.spanX = 1 + collection.spanY = 1 + if (collection is FolderInfo) { + collection.options = c.options + } else { + // An app pair may be inside another folder, so it needs to preserve rank information. + collection.rank = c.rank + } - // no special handling required for restored folders c.markRestored() - c.checkAndAddItem(folderInfo, bgDataModel, memoryLogger) + c.checkAndAddItem(collection, bgDataModel, memoryLogger) } /** diff --git a/src/com/android/launcher3/model/data/AppPairInfo.kt b/src/com/android/launcher3/model/data/AppPairInfo.kt new file mode 100644 index 0000000000..408131683e --- /dev/null +++ b/src/com/android/launcher3/model/data/AppPairInfo.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.data + +import android.content.Context +import com.android.launcher3.LauncherSettings +import com.android.launcher3.logger.LauncherAtom +import com.android.launcher3.views.ActivityContext + +/** A type of app collection that launches multiple apps into split screen. */ +class AppPairInfo() : CollectionInfo() { + init { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR + } + + /** Convenience constructor, calls primary constructor and init block */ + constructor(app1: WorkspaceItemInfo, app2: WorkspaceItemInfo) : this() { + add(app1) + add(app2) + } + + /** Adds an element to the contents array. */ + override fun add(item: WorkspaceItemInfo) { + contents.add(item) + } + + /** Returns the first app in the pair. */ + fun getFirstApp() = contents[0] + + /** Returns the second app in the pair. */ + fun getSecondApp() = contents[1] + + /** Returns if either of the app pair members is currently disabled. */ + override fun isDisabled() = anyMatch { it.isDisabled } + + /** Checks if the app pair is launchable at the current screen size. */ + fun isLaunchable(context: Context) = + (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile().isTablet || + noneMatch { it.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE) } + + /** Generates an ItemInfo for logging. */ + override fun buildProto(cInfo: CollectionInfo?): LauncherAtom.ItemInfo { + val appPairIcon = LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size) + appPairIcon.setLabelInfo(title.toString()) + return getDefaultItemInfoBuilder() + .setFolderIcon(appPairIcon) + .setRank(rank) + .setContainerInfo(getContainerInfo()) + .build() + } +} diff --git a/src/com/android/launcher3/model/data/CollectionInfo.kt b/src/com/android/launcher3/model/data/CollectionInfo.kt new file mode 100644 index 0000000000..2b865a574e --- /dev/null +++ b/src/com/android/launcher3/model/data/CollectionInfo.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 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.data + +import com.android.launcher3.LauncherSettings +import com.android.launcher3.logger.LauncherAtom +import com.android.launcher3.util.ContentWriter +import java.util.function.Predicate + +abstract class CollectionInfo : ItemInfo() { + var contents: ArrayList = ArrayList() + + abstract fun add(item: WorkspaceItemInfo) + + /** Convenience function. Checks contents to see if any match a given predicate. */ + fun anyMatch(matcher: Predicate): Boolean { + return contents.stream().anyMatch(matcher) + } + + /** Convenience function. Returns true if none of the contents match a given predicate. */ + fun noneMatch(matcher: Predicate): Boolean { + return contents.stream().noneMatch(matcher) + } + + override fun onAddToDatabase(writer: ContentWriter) { + super.onAddToDatabase(writer) + writer.put(LauncherSettings.Favorites.TITLE, title) + } + + /** Returns the collection wrapped as {@link LauncherAtom.ItemInfo} for logging. */ + override fun buildProto(): LauncherAtom.ItemInfo { + return buildProto(null) + } +} diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java index 83ba2b3a9f..1bbb2fe971 100644 --- a/src/com/android/launcher3/model/data/FolderInfo.java +++ b/src/com/android/launcher3/model/data/FolderInfo.java @@ -24,8 +24,6 @@ import static com.android.launcher3.logger.LauncherAtom.Attribute.EMPTY_LABEL; import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL; import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL; -import android.os.Process; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -49,7 +47,7 @@ import java.util.stream.IntStream; /** * Represents a folder containing shortcuts or apps. */ -public class FolderInfo extends ItemInfo { +public class FolderInfo extends CollectionInfo { public static final int NO_FLAGS = 0x00000000; @@ -100,27 +98,15 @@ public class FolderInfo extends ItemInfo { public FolderNameInfos suggestedFolderNames; - /** - * The apps and shortcuts - */ - public ArrayList contents = new ArrayList<>(); - private ArrayList mListeners = new ArrayList<>(); public FolderInfo() { itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; - user = Process.myUserHandle(); } - /** - * Create an app pair, a type of app collection that launches multiple apps into split screen - */ - public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) { - FolderInfo newAppPair = new FolderInfo(); - newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; - newAppPair.add(app1, /* animate */ false); - newAppPair.add(app2, /* animate */ false); - return newAppPair; + /** Adds a app or shortcut to the contents array without animation. */ + public void add(@NonNull WorkspaceItemInfo item) { + add(item, false /* animate */); } /** @@ -129,15 +115,15 @@ public class FolderInfo extends ItemInfo { * @param item */ public void add(WorkspaceItemInfo item, boolean animate) { - add(item, contents.size(), animate); + add(item, getContents().size(), animate); } /** * Add an app or shortcut for a specified rank. */ public void add(WorkspaceItemInfo item, int rank, boolean animate) { - rank = Utilities.boundToRange(rank, 0, contents.size()); - contents.add(rank, item); + rank = Utilities.boundToRange(rank, 0, getContents().size()); + getContents().add(rank, item); for (int i = 0; i < mListeners.size(); i++) { mListeners.get(i).onAdd(item, rank); } @@ -157,7 +143,7 @@ public class FolderInfo extends ItemInfo { * Remove all matching app or shortcut. Does not change the DB. */ public void removeAll(List items, boolean animate) { - contents.removeAll(items); + getContents().removeAll(items); for (int i = 0; i < mListeners.size(); i++) { mListeners.get(i).onRemove(items); } @@ -167,8 +153,7 @@ public class FolderInfo extends ItemInfo { @Override public void onAddToDatabase(@NonNull ContentWriter writer) { super.onAddToDatabase(writer); - writer.put(LauncherSettings.Favorites.TITLE, title) - .put(LauncherSettings.Favorites.OPTIONS, options); + writer.put(LauncherSettings.Favorites.OPTIONS, options); } public void addListener(FolderListener listener) { @@ -219,9 +204,9 @@ public class FolderInfo extends ItemInfo { @NonNull @Override - public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo fInfo) { + public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo cInfo) { FolderIcon.Builder folderIcon = FolderIcon.newBuilder() - .setCardinality(contents.size()); + .setCardinality(getContents().size()); if (LabelState.SUGGESTED.equals(getLabelState())) { folderIcon.setLabelInfo(title.toString()); } @@ -278,19 +263,10 @@ public class FolderInfo extends ItemInfo { public ItemInfo makeShallowCopy() { FolderInfo folderInfo = new FolderInfo(); folderInfo.copyFrom(this); - folderInfo.contents = this.contents; + folderInfo.setContents(this.getContents()); return folderInfo; } - /** - * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging. - */ - @NonNull - @Override - public LauncherAtom.ItemInfo buildProto() { - return buildProto(null); - } - /** * Returns index of the accepted suggestion. */ @@ -371,13 +347,4 @@ public class FolderInfo extends ItemInfo { } return LauncherAtom.ToState.TO_STATE_UNSPECIFIED; } - - @Override - public boolean isDisabled() { - if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) { - return contents.stream().anyMatch((WorkspaceItemInfo::isDisabled)); - } - - return super.isDisabled(); - } } diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index f7cff78ba1..8c3efd7fde 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -349,10 +349,9 @@ public class ItemInfo { /** * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info. - * @param fInfo */ @NonNull - public LauncherAtom.ItemInfo buildProto(@Nullable final FolderInfo fInfo) { + public LauncherAtom.ItemInfo buildProto(@Nullable final CollectionInfo cInfo) { LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder(); Optional nullableComponent = Optional.ofNullable(getTargetComponent()); switch (itemType) { @@ -398,21 +397,21 @@ public class ItemInfo { default: break; } - if (fInfo != null) { + if (cInfo != null) { LauncherAtom.FolderContainer.Builder folderBuilder = LauncherAtom.FolderContainer.newBuilder(); folderBuilder.setGridX(cellX).setGridY(cellY).setPageIndex(screenId); - switch (fInfo.container) { + switch (cInfo.container) { case CONTAINER_HOTSEAT: case CONTAINER_HOTSEAT_PREDICTION: folderBuilder.setHotseat(LauncherAtom.HotseatContainer.newBuilder() - .setIndex(fInfo.screenId)); + .setIndex(cInfo.screenId)); break; case CONTAINER_DESKTOP: folderBuilder.setWorkspace(LauncherAtom.WorkspaceContainer.newBuilder() - .setPageIndex(fInfo.screenId) - .setGridX(fInfo.cellX).setGridY(fInfo.cellY)); + .setPageIndex(cInfo.screenId) + .setGridX(cInfo.cellX).setGridY(cInfo.cellY)); break; } itemBuilder.setContainerInfo(ContainerInfo.newBuilder().setFolder(folderBuilder)); diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java index 6fa8c546c9..f4dda5593a 100644 --- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java @@ -271,8 +271,8 @@ public class LauncherAppWidgetInfo extends ItemInfo { @NonNull @Override - public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo folderInfo) { - LauncherAtom.ItemInfo info = super.buildProto(folderInfo); + public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) { + LauncherAtom.ItemInfo info = super.buildProto(collectionInfo); return info.toBuilder() .setWidget(info.getWidget().toBuilder().setWidgetFeatures(widgetFeatures)) .addItemAttributes(getAttribute(sourceContainer)) diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index 911568c3b6..0c25e96aa0 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -53,6 +53,7 @@ import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; @@ -101,11 +102,9 @@ public class ItemClickHandler { if (tag instanceof WorkspaceItemInfo) { onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher); } else if (tag instanceof FolderInfo) { - if (v instanceof FolderIcon) { - onClickFolderIcon(v); - } else if (v instanceof AppPairIcon) { - onClickAppPairIcon(v); - } + onClickFolderIcon(v); + } else if (tag instanceof AppPairInfo) { + onClickAppPairIcon(v); } else if (tag instanceof AppInfo) { startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher); } else if (tag instanceof LauncherAppWidgetInfo) { @@ -150,7 +149,7 @@ public class ItemClickHandler { private static void onClickAppPairIcon(View v) { Launcher launcher = Launcher.getLauncher(v.getContext()); AppPairIcon appPairIcon = (AppPairIcon) v; - if (!appPairIcon.isLaunchableAtScreenSize()) { + if (!appPairIcon.getInfo().isLaunchable(launcher)) { // Display a message for app pairs that are disabled due to screen size boolean isFoldable = InvariantDeviceProfile.INSTANCE.get(launcher) .supportedProfiles.stream().anyMatch(dp -> dp.isTwoPanels); @@ -159,8 +158,8 @@ public class ItemClickHandler { : R.string.app_pair_unlaunchable_at_screen_size, Toast.LENGTH_SHORT).show(); } else if (appPairIcon.getInfo().isDisabled()) { - WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0); - WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1); + WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp(); + WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp(); // Show the user why the app pair is disabled. if (app1.isDisabled() && !handleDisabledItemClicked(app1, launcher)) { // If handleDisabledItemClicked() did not handle the error message, we initiate an diff --git a/src/com/android/launcher3/util/ItemInflater.kt b/src/com/android/launcher3/util/ItemInflater.kt index 0f8311d50a..ebf4656c61 100644 --- a/src/com/android/launcher3/util/ItemInflater.kt +++ b/src/com/android/launcher3/util/ItemInflater.kt @@ -29,6 +29,7 @@ import com.android.launcher3.R import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.folder.FolderIcon import com.android.launcher3.model.ModelWriter +import com.android.launcher3.model.data.AppPairInfo import com.android.launcher3.model.data.FolderInfo import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.model.data.LauncherAppWidgetInfo @@ -81,7 +82,7 @@ class ItemInflater( R.layout.app_pair_icon, context, parent, - item as FolderInfo, + item as AppPairInfo, BubbleTextView.DISPLAY_WORKSPACE ) Favorites.ITEM_TYPE_APPWIDGET, diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java index 307411151e..063313a8c6 100644 --- a/src/com/android/launcher3/util/ItemInfoMatcher.java +++ b/src/com/android/launcher3/util/ItemInfoMatcher.java @@ -65,19 +65,10 @@ public abstract class ItemInfoMatcher { * Returns a matcher for items within folders. */ public static Predicate forFolderMatch(Predicate childOperator) { - return info -> info instanceof FolderInfo && ((FolderInfo) info).contents.stream() + return info -> info instanceof FolderInfo && ((FolderInfo) info).getContents().stream() .anyMatch(childOperator); } - /** - * Returns a matcher for items within app pairs. - */ - public static Predicate forAppPairMatch(Predicate childOperator) { - Predicate isAppPair = info -> - info instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR; - return isAppPair.and(forFolderMatch(childOperator)); - } - /** * Returns a matcher for items with provided ids */ diff --git a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java index 69786bb5e5..02779ce596 100644 --- a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java +++ b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java @@ -23,6 +23,7 @@ import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.graphics.PreloadIconDrawable; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -59,7 +60,7 @@ public interface LauncherBindableItemsContainer { : null); } else if (info instanceof FolderInfo && v instanceof FolderIcon) { ((FolderIcon) v).updatePreviewItems(updates::contains); - } else if (info instanceof FolderInfo && v instanceof AppPairIcon appPairIcon) { + } else if (info instanceof AppPairInfo && v instanceof AppPairIcon appPairIcon) { appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains); } @@ -89,7 +90,7 @@ public interface LauncherBindableItemsContainer { ((PendingAppWidgetHostView) v).applyState(); } else if (v instanceof FolderIcon && info instanceof FolderInfo) { ((FolderIcon) v).updatePreviewItems(updates::contains); - } else if (info instanceof FolderInfo && v instanceof AppPairIcon appPairIcon) { + } else if (info instanceof AppPairInfo && v instanceof AppPairIcon appPairIcon) { appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains); } // process all the shortcuts diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java index a501960920..a9162527af 100644 --- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -25,7 +25,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.LauncherSettings; import com.android.launcher3.PendingAddItemInfo; import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.widget.picker.WidgetRecommendationCategory; import com.android.launcher3.widget.util.WidgetSizes; @@ -82,8 +82,8 @@ public class PendingAddWidgetInfo extends PendingAddItemInfo { @NonNull @Override - public LauncherAtom.ItemInfo buildProto(@Nullable FolderInfo folderInfo) { - LauncherAtom.ItemInfo info = super.buildProto(folderInfo); + public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) { + LauncherAtom.ItemInfo info = super.buildProto(collectionInfo); return info.toBuilder() .addItemAttributes(LauncherAppWidgetInfo.getAttribute(sourceContainer)) .build(); diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java index b86333c149..0c3081fa83 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java @@ -26,7 +26,7 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; import com.android.launcher3.model.ModelDbController; -import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; import com.android.launcher3.util.ContentWriter; @@ -73,9 +73,8 @@ public class FavoriteItemsTransaction { ContentWriter writer = new ContentWriter(mContext); ItemInfo item = mItemsToSubmit.get(i).get(); - if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { - FolderInfo folderInfo = (FolderInfo) item; - for (ItemInfo itemInfo : folderInfo.contents) { + if (item instanceof CollectionInfo ci) { + for (ItemInfo itemInfo : ci.getContents()) { itemInfo.container = i; containerItems.add(itemInfo); } diff --git a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt index 4ec5b0e404..3a1883cb02 100644 --- a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt +++ b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt @@ -28,6 +28,7 @@ import com.android.launcher3.LauncherPrefs.Companion.get import com.android.launcher3.icons.BaseIconFactory import com.android.launcher3.icons.FastBitmapDrawable import com.android.launcher3.icons.UserBadgeDrawable +import com.android.launcher3.model.data.FolderInfo import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.util.ActivityContextWrapper import com.android.launcher3.util.FlagOp @@ -71,8 +72,8 @@ class PreviewItemManagerTest { .build() ) .loadModelSync() - folderItems = modelHelper.bgDataModel.folders.valueAt(0).contents - folderIcon.mInfo = modelHelper.bgDataModel.folders.valueAt(0) + folderItems = modelHelper.bgDataModel.collections.valueAt(0).contents + folderIcon.mInfo = modelHelper.bgDataModel.collections.valueAt(0) as FolderInfo folderIcon.mInfo.contents = folderItems // Set first icon to be themed. diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java index 7a0b60ab68..abb0c39d56 100644 --- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java +++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -160,6 +160,6 @@ public class CacheDataUpdatedTaskTest { } private List allItems() { - return ((FolderInfo) mModelHelper.getBgDataModel().itemsIdMap.get(1)).contents; + return ((FolderInfo) mModelHelper.getBgDataModel().itemsIdMap.get(1)).getContents(); } } diff --git a/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java index 2b893211dc..10785f7406 100644 --- a/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java +++ b/tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java @@ -87,7 +87,7 @@ public class DefaultLayoutProviderTest { assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size()); ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0); assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType); - assertEquals(3, ((FolderInfo) info).contents.size()); + assertEquals(3, ((FolderInfo) info).getContents().size()); } @Test @@ -102,7 +102,7 @@ public class DefaultLayoutProviderTest { assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size()); ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0); assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType); - assertEquals(3, ((FolderInfo) info).contents.size()); + assertEquals(3, ((FolderInfo) info).getContents().size()); assertEquals("CustomFolder", info.title.toString()); } @@ -154,11 +154,11 @@ public class DefaultLayoutProviderTest { // Verify folder assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size()); FolderInfo info = (FolderInfo) mModelHelper.getBgDataModel().workspaceItems.get(0); - assertEquals(3, info.contents.size()); + assertEquals(3, info.getContents().size()); // Verify last icon assertEquals(LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT, - info.contents.get(info.contents.size() - 1).itemType); + info.getContents().get(info.getContents().size() - 1).itemType); } private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception { diff --git a/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt index 2118ed6721..ed587a1d33 100644 --- a/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt +++ b/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt @@ -165,11 +165,11 @@ class FolderIconLoadTest { // Reload again with correct icon state app.model.forceReload() modelHelper.loadModelSync() - val folders = modelHelper.getBgDataModel().folders + val collections = modelHelper.getBgDataModel().collections - assertThat(folders.size()).isEqualTo(1) - assertThat(folders.valueAt(0).contents.size).isEqualTo(itemCount) - return folders.valueAt(0).contents + assertThat(collections.size()).isEqualTo(1) + assertThat(collections.valueAt(0).contents.size).isEqualTo(itemCount) + return collections.valueAt(0).contents } private fun verifyHighRes(items: ArrayList, vararg indices: Int) { diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt index e2ca31f22b..3d10a8564b 100644 --- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt +++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt @@ -103,7 +103,7 @@ class LoaderTaskTest { .runSyncOnBackgroundThread() Truth.assertThat(workspaceItems.size).isAtLeast(25) Truth.assertThat(appWidgets.size).isAtLeast(7) - Truth.assertThat(folders.size()).isAtLeast(8) + Truth.assertThat(collections.size()).isAtLeast(8) Truth.assertThat(itemsIdMap.size()).isAtLeast(40) } diff --git a/tests/src/com/android/launcher3/util/ItemInflaterTest.kt b/tests/src/com/android/launcher3/util/ItemInflaterTest.kt index efad899e35..00655276b9 100644 --- a/tests/src/com/android/launcher3/util/ItemInflaterTest.kt +++ b/tests/src/com/android/launcher3/util/ItemInflaterTest.kt @@ -36,6 +36,7 @@ import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.folder.FolderIcon import com.android.launcher3.model.ModelWriter import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.AppPairInfo import com.android.launcher3.model.data.FolderInfo import com.android.launcher3.model.data.LauncherAppWidgetInfo import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_ID_NOT_VALID @@ -170,7 +171,7 @@ class ItemInflaterTest { @Test fun test_app_pair_inflated_on_UI() { - val itemInfo = FolderInfo() + val itemInfo = AppPairInfo() itemInfo.itemType = ITEM_TYPE_APP_PAIR itemInfo.contents.add(workspaceItemInfo()) itemInfo.contents.add(workspaceItemInfo()) @@ -186,7 +187,7 @@ class ItemInflaterTest { fun test_app_pair_inflated_on_BG() { setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION) - val itemInfo = FolderInfo() + val itemInfo = AppPairInfo() itemInfo.itemType = ITEM_TYPE_APP_PAIR itemInfo.contents.add(workspaceItemInfo()) itemInfo.contents.add(workspaceItemInfo())