diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index 66243985dc..af68545cc8 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -53,8 +53,8 @@ import com.android.launcher3.graphics.DragPreviewProvider; import com.android.launcher3.logger.LauncherAtom.ContainerInfo; import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer; import com.android.launcher3.logging.InstanceId; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.popup.SystemShortcut; @@ -285,8 +285,8 @@ public class HotseatPredictionController implements DragController.DragListener, /** * Sets or updates the predicted items */ - public void setPredictedItems(FixedContainerItems items) { - mPredictedItems = new ArrayList(items.items); + public void setPredictedItems(PredictedContainerInfo items) { + mPredictedItems = items.getContents(); if (mPredictedItems.isEmpty()) { HotseatRestoreHelper.restoreBackup(mLauncher); } diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java index 70868c5ab2..c0d1fe4465 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java @@ -25,7 +25,6 @@ import android.content.Context; import android.os.Bundle; import com.android.launcher3.model.BgDataModel; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.PredictionHelper; import com.android.launcher3.model.data.ItemInfo; @@ -58,12 +57,9 @@ public class HotseatPredictionModel { .collect(Collectors.toCollection(ArrayList::new)); ArrayList currentTargets = new ArrayList<>(); - FixedContainerItems hotseatItems = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION); - if (hotseatItems != null) { - for (ItemInfo itemInfo : hotseatItems.items) { - AppTarget target = getAppTargetFromItemInfo(context, itemInfo); - if (target != null) currentTargets.add(target); - } + for (ItemInfo itemInfo : dataModel.getPredictedContents(CONTAINER_HOTSEAT_PREDICTION)) { + AppTarget target = getAppTargetFromItemInfo(context, itemInfo); + if (target != null) currentTargets.add(target); } bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, events); bundle.putParcelableArrayList(BUNDLE_KEY_CURRENT_ITEMS, currentTargets); diff --git a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java index ce02bd7384..5dacd3ad3b 100644 --- a/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/PredictionUpdateTask.java @@ -34,12 +34,13 @@ import com.android.launcher3.ConstantItem; import com.android.launcher3.LauncherModel.ModelUpdateTask; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.icons.IconCache; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -70,7 +71,7 @@ public class PredictionUpdateTask implements ModelUpdateTask { LauncherPrefs.get(context).put(LAST_PREDICTION_ENABLED, !mTargets.isEmpty()); Set usersForChangedShortcuts = - dataModel.extraItems.get(mPredictorState.containerId).items.stream() + dataModel.getPredictedContents(mPredictorState.containerId).stream() .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT) .map(info -> info.user) .collect(Collectors.toSet()); @@ -118,12 +119,12 @@ public class PredictionUpdateTask implements ModelUpdateTask { items.add(itemInfo); } - FixedContainerItems fci = new FixedContainerItems(mPredictorState.containerId, items); - dataModel.extraItems.put(fci.containerId, fci); - taskController.bindExtraContainerItems(fci); + PredictedContainerInfo pci = new PredictedContainerInfo(mPredictorState.containerId, items); + dataModel.itemsIdMap.put(pci.id, pci); + taskController.bindUpdatedWorkspaceItems(Collections.singleton(pci)); usersForChangedShortcuts.forEach(u -> dataModel.updateShortcutPinnedState(context, u)); // Save to disk - mPredictorState.storage.write(context, fci.items); + mPredictorState.storage.write(context, pci.getContents()); } } diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 3028f9bf35..c59c6ce29c 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -20,8 +20,8 @@ import static android.text.format.DateUtils.formatElapsedTime; 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_ALL_APPS_PREDICTION; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG; import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle; @@ -55,9 +55,9 @@ import com.android.launcher3.dagger.ApplicationContext; import com.android.launcher3.logger.LauncherAtom; 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.CollectionInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.IntSparseArrayMap; import com.android.quickstep.logging.SettingsChangeLogger; @@ -142,18 +142,18 @@ public class QuickstepModelDelegate extends ModelDelegate { private void loadAndBindPredictedItems( int numColumns, @NonNull PredictorState state) { PredictedItemFactory parser = mItemParserFactory.newParser(numColumns, state); - FixedContainerItems fci = new FixedContainerItems(state.containerId, + PredictedContainerInfo fci = new PredictedContainerInfo(state.containerId, state.storage.read(mContext, parser, mUserCache::getUserForSerialNumber)); - mDataModel.extraItems.put(state.containerId, fci); + mDataModel.itemsIdMap.put(state.containerId, fci); } @CallSuper @Override public void loadAndBindOtherItems() { - FixedContainerItems widgetPredictionFCI = new FixedContainerItems( + PredictedContainerInfo widgetPredictionFCI = new PredictedContainerInfo( mWidgetsRecommendationState.containerId, new ArrayList<>()); // Widgets prediction isn't used frequently. And thus, it is not persisted on disk. - mDataModel.extraItems.put(mWidgetsRecommendationState.containerId, widgetPredictionFCI); + mDataModel.itemsIdMap.put(mWidgetsRecommendationState.containerId, widgetPredictionFCI); } public void markActive() { diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java index fb3e9d477f..d4de22574c 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java @@ -31,8 +31,8 @@ import androidx.annotation.NonNull; import com.android.launcher3.LauncherModel.ModelUpdateTask; import com.android.launcher3.R; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider; @@ -133,11 +133,11 @@ public final class WidgetsPredictionUpdateTask implements ModelUpdateTask { .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, categoryProvider.getWidgetRecommendationCategory(context, it))) .collect(Collectors.toList()); - FixedContainerItems fixedContainerItems = - new FixedContainerItems(mPredictorState.containerId, items); + PredictedContainerInfo pci = + new PredictedContainerInfo(mPredictorState.containerId, items); - dataModel.extraItems.put(mPredictorState.containerId, fixedContainerItems); - taskController.bindExtraContainerItems(fixedContainerItems); + dataModel.itemsIdMap.put(mPredictorState.containerId, pci); + taskController.bindUpdatedWorkspaceItems(Collections.singleton(pci)); // Don't store widgets prediction to disk because it is not used frequently. } diff --git a/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegateImpl.java b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegateImpl.java index 99d83256f5..17d33f6153 100644 --- a/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegateImpl.java +++ b/quickstep/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegateImpl.java @@ -22,7 +22,7 @@ import android.window.DesktopExperienceFlags; import com.android.launcher3.appprediction.AppsDividerView; import com.android.launcher3.appprediction.PredictionRowView; -import com.android.launcher3.model.BgDataModel; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.views.ActivityContext; /** @@ -48,10 +48,10 @@ public final class SecondaryDisplayQuickstepDelegateImpl extends SecondaryDispla } @Override - public void setPredictedApps(BgDataModel.FixedContainerItems item) { + public void setPredictedApps(PredictedContainerInfo info) { mActivityContext.getAppsView().getFloatingHeaderView() .findFixedRowByType(PredictionRowView.class) - .setPredictedApps(item.items); + .setPredictedApps(info.getContents()); } @Override diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index 987271e07d..134e7f844a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.taskbar; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; @@ -28,10 +29,9 @@ import androidx.annotation.UiThread; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.celllayout.CellInfo; import com.android.launcher3.model.BgDataModel; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; -import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.taskbar.TaskbarView.TaskbarLayoutParams; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntSparseArrayMap; @@ -79,18 +79,14 @@ public class TaskbarModelCallbacks implements } @Override - public void bindCompleteModel(IntSparseArrayMap itemIdMap, - List extraItems, StringCache stringCache, boolean isBindingSync) { + public void bindCompleteModel(IntSparseArrayMap itemIdMap, boolean isBindingSync) { mHotseatItems.clear(); - mPredictedItems = Collections.emptyList(); + mPredictedItems = itemIdMap.get(CONTAINER_HOTSEAT_PREDICTION) + instanceof PredictedContainerInfo pci ? pci.getContents() : Collections.emptyList(); handleItemsAdded(itemIdMap); - for (FixedContainerItems item: extraItems) { - if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { - mPredictedItems = item.items; - } else if (item.containerId == Favorites.CONTAINER_ALL_APPS_PREDICTION) { - mControllers.taskbarAllAppsController.setPredictedApps(item.items); - } + if (itemIdMap.get(CONTAINER_ALL_APPS_PREDICTION) instanceof PredictedContainerInfo pci) { + mControllers.taskbarAllAppsController.setPredictedApps(pci.getContents()); } commitItemsToUI(); } @@ -114,12 +110,23 @@ public class TaskbarModelCallbacks implements } @Override - public void bindItemsUpdated(Set updates) { + public void bindItemsUpdated(@NonNull Set updates) { Set itemsToRebind = updateContainerItems(updates, mContext); - boolean removed = handleItemsRemoved(ItemInfoMatcher.ofItems(itemsToRebind)); boolean added = handleItemsAdded(itemsToRebind); - if (removed || added) { + + boolean predictionsUpdated = false; + for (ItemInfo update: updates) { + if (update instanceof PredictedContainerInfo pci) { + if (pci.id == Favorites.CONTAINER_HOTSEAT_PREDICTION) { + mPredictedItems = pci.getContents(); + predictionsUpdated = true; + } else if (pci.id == CONTAINER_ALL_APPS_PREDICTION) { + mControllers.taskbarAllAppsController.setPredictedApps(pci.getContents()); + } + } + } + if (removed || added || predictionsUpdated) { commitItemsToUI(); } } @@ -165,16 +172,6 @@ public class TaskbarModelCallbacks implements return modified; } - @Override - public void bindExtraContainerItems(FixedContainerItems item) { - if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { - mPredictedItems = item.items; - commitItemsToUI(); - } else if (item.containerId == Favorites.CONTAINER_ALL_APPS_PREDICTION) { - mControllers.taskbarAllAppsController.setPredictedApps(item.items); - } - } - private void commitItemsToUI() { ItemInfo[] hotseatItemInfos = new ItemInfo[mContext.getDeviceProfile().numShownHotseatIcons]; diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 2ded6bd0e3..687f7f670e 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -133,9 +133,9 @@ import com.android.launcher3.hybridhotseat.HotseatPredictionController; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.WellbeingModel; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.proxy.ProxyActivityStarter; import com.android.launcher3.statehandlers.DepthController; @@ -238,7 +238,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, protected static final String RING_APPEAR_ANIMATION_PREFIX = "RingAppearAnimation\t"; - private FixedContainerItems mAllAppsPredictions; + private PredictedContainerInfo mAllAppsPredictions; private HotseatPredictionController mHotseatPredictionController; private DepthController mDepthController; private QuickstepTransitionManager mAppTransitionManager; @@ -358,16 +358,16 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, if (mAllAppsPredictions != null && (info.itemType == ITEM_TYPE_APPLICATION || info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) { - int count = mAllAppsPredictions.items.size(); + List items = mAllAppsPredictions.getContents(); + int count = items.size(); for (int i = 0; i < count; i++) { - ItemInfo targetInfo = mAllAppsPredictions.items.get(i); + ItemInfo targetInfo = items.get(i); if (targetInfo.itemType == info.itemType && targetInfo.user.equals(info.user) && Objects.equals(targetInfo.getIntent(), info.getIntent())) { logger.withRank(i); break; } - } } logger.log(LAUNCHER_APP_LAUNCH_TAP); @@ -556,17 +556,20 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, } @Override - public void bindExtraContainerItems(FixedContainerItems item) { - if (item.containerId == Favorites.CONTAINER_ALL_APPS_PREDICTION) { - mAllAppsPredictions = item; - PredictionRowView predictionRowView = - getAppsView().getFloatingHeaderView().findFixedRowByType( - PredictionRowView.class); - predictionRowView.setPredictedApps(item.items); - } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) { - mHotseatPredictionController.setPredictedItems(item); - } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) { - getWidgetPickerDataProvider().setWidgetRecommendations(item.items); + public void bindPredictedContainerInfo(PredictedContainerInfo info) { + super.bindPredictedContainerInfo(info); + switch (info.id) { + case Favorites.CONTAINER_ALL_APPS_PREDICTION: + mAllAppsPredictions = info; + getAppsView().getFloatingHeaderView().findFixedRowByType( + PredictionRowView.class).setPredictedApps(info.getContents()); + break; + case Favorites.CONTAINER_HOTSEAT_PREDICTION: + mHotseatPredictionController.setPredictedItems(info); + break; + case Favorites.CONTAINER_WIDGETS_PREDICTION: + getWidgetPickerDataProvider().setWidgetRecommendations(info.getContents()); + break; } } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionUpdateTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionUpdateTaskTest.kt new file mode 100644 index 0000000000..fac9343537 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/PredictionUpdateTaskTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model + +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetId +import android.os.Process.myUserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.icons.cache.CacheLookupFlag.Companion.DEFAULT_LOOKUP_FLAG +import com.android.launcher3.util.AllModulesForTest +import com.android.launcher3.util.Executors.MODEL_EXECUTOR +import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY +import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2 +import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE +import com.android.launcher3.util.SandboxApplication +import com.android.launcher3.util.TestUtil +import com.google.common.truth.Truth.assertThat +import dagger.Component +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PredictionUpdateTaskTest { + + @get:Rule val context = SandboxApplication().withModelDependency() + + private val containerId = -300 + private val predictorState = PredictorState(containerId, "test-storage", DEFAULT_LOOKUP_FLAG) + + lateinit var component: TestComponent + + @Before + fun setup() { + context.initDaggerComponent(DaggerPredictionUpdateTaskTest_TestComponent.builder()) + component = context.appComponent as TestComponent + } + + @Test + fun emptyPredictions_update_data_model() { + assertThat(component.getDataModel().itemsIdMap[containerId]).isNull() + + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + PredictionUpdateTask(predictorState, listOf()) + .execute( + component.getTaskController(), + component.getDataModel(), + component.getAllAppsList(), + ) + } + assertThat(component.getDataModel().itemsIdMap[containerId]).isNotNull() + assertThat(component.getDataModel().getPredictedContents(containerId)).isEmpty() + } + + @Test + fun validPredictions_added_to_data_model() { + assertThat(component.getDataModel().itemsIdMap[containerId]).isNull() + + TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { + PredictionUpdateTask( + predictorState, + listOf(createAppTarget(TEST_ACTIVITY), createAppTarget(TEST_ACTIVITY2)), + ) + .execute( + component.getTaskController(), + component.getDataModel(), + component.getAllAppsList(), + ) + } + assertThat(component.getDataModel().itemsIdMap[containerId]).isNotNull() + val items = component.getDataModel().getPredictedContents(containerId) + assertThat(items).hasSize(2) + } + + private fun createAppTarget(className: String): AppTarget = + AppTarget.Builder(AppTargetId("app:$className"), TEST_PACKAGE, myUserHandle()) + .setClassName(className) + .build() + + @LauncherAppSingleton + @Component(modules = [AllModulesForTest::class]) + interface TestComponent : LauncherAppComponent { + + fun getDataModel(): BgDataModel + + fun getAllAppsList(): AllAppsList + + fun getTaskController(): ModelTaskController + + @Component.Builder + interface Builder : LauncherAppComponent.Builder { + + override fun build(): TestComponent + } + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt index 43cc281490..9ae7edee15 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt @@ -31,8 +31,6 @@ import com.android.launcher3.R import com.android.launcher3.dagger.LauncherAppSingleton import com.android.launcher3.dagger.LauncherComponentProvider.appComponent import com.android.launcher3.model.BgDataModel -import com.android.launcher3.model.BgDataModel.FixedContainerItems -import com.android.launcher3.model.StringCache import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.model.data.TaskItemInfo import com.android.launcher3.model.data.WorkspaceItemInfo @@ -699,8 +697,6 @@ class TaskbarOverflowTest { override fun bindCompleteModel( itemIdMap: IntSparseArrayMap, - extraItems: List, - stringCache: StringCache, isBindingSync: Boolean, ) = bindItemsAdded(itemIdMap.toList()) diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java index 4efa4ea9b8..69499fbb76 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -48,13 +48,15 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.Flags; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.ModelTestExtensions; import com.android.launcher3.util.SandboxApplication; @@ -71,6 +73,7 @@ import org.mockito.junit.MockitoRule; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @SmallTest @@ -184,7 +187,8 @@ public final class WidgetsPredicationUpdateTaskTest { // 2. app3 doesn't have a widget. // 3. only 1 widget is picked from app1 because we only want to promote one widget // per app. - List recommendedWidgets = mCallback.mRecommendedWidgets.items + List recommendedWidgets = mCallback.mRecommendedWidgets + .getContents() .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); @@ -220,7 +224,8 @@ public final class WidgetsPredicationUpdateTaskTest { runOnExecutorSync(MAIN_EXECUTOR, () -> { }); // Only widgets suggested by prediction system are returned. - List recommendedWidgets = mCallback.mRecommendedWidgets.items + List recommendedWidgets = mCallback.mRecommendedWidgets + .getContents() .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); @@ -245,7 +250,8 @@ public final class WidgetsPredicationUpdateTaskTest { runOnExecutorSync(MAIN_EXECUTOR, () -> { }); // Only widget 1 (and no widget 6 as its meant to be hidden from picker). - List recommendedWidgets = mCallback.mRecommendedWidgets.items + List recommendedWidgets = mCallback.mRecommendedWidgets + .getContents() .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); @@ -273,11 +279,16 @@ public final class WidgetsPredicationUpdateTaskTest { private final class FakeBgDataModelCallback implements BgDataModel.Callbacks { - private FixedContainerItems mRecommendedWidgets = null; + private PredictedContainerInfo mRecommendedWidgets = null; @Override - public void bindExtraContainerItems(FixedContainerItems item) { - mRecommendedWidgets = item; + public void bindItemsUpdated(@NonNull Set updates) { + for (ItemInfo update : updates) { + if (update.id == CONTAINER_WIDGETS_PREDICTION + && update instanceof PredictedContainerInfo pci) { + mRecommendedWidgets = pci; + } + } } } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 9456a8ca2d..d20a683b66 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -201,7 +201,6 @@ import com.android.launcher3.logging.StartupLatencyLogger; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent; import com.android.launcher3.model.BgDataModel.Callbacks; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.ItemInstallQueue; import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.StringCache; @@ -210,6 +209,7 @@ 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; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.pm.PinRequestHelper; @@ -2198,9 +2198,9 @@ public class Launcher extends StatefulActivity } @Override - public void bindCompleteModelAsync(IntSparseArrayMap itemIdMap, - List extraItems, StringCache stringCache, boolean isBindingSync) { - mModelCallbacks.bindCompleteModelAsync(itemIdMap, extraItems, stringCache, isBindingSync); + public void bindCompleteModelAsync( + IntSparseArrayMap itemIdMap, boolean isBindingSync) { + mModelCallbacks.bindCompleteModelAsync(itemIdMap, isBindingSync); } @Override @@ -2493,6 +2493,9 @@ public class Launcher extends StatefulActivity mModelCallbacks.bindStringCache(cache); } + /** Called to updated any prediction info by the {@link #mModelCallbacks} */ + public void bindPredictedContainerInfo(PredictedContainerInfo info) { } + /** * @param packageUser if null, refreshes all widgets and shortcuts, otherwise only * refreshes the widgets and shortcuts associated with the given package/user diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt index 3a68afe48c..a84d8ad205 100644 --- a/src/com/android/launcher3/ModelCallbacks.kt +++ b/src/com/android/launcher3/ModelCallbacks.kt @@ -16,7 +16,6 @@ import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID import com.android.launcher3.allapps.AllAppsStore import com.android.launcher3.config.FeatureFlags import com.android.launcher3.model.BgDataModel -import com.android.launcher3.model.BgDataModel.FixedContainerItems import com.android.launcher3.model.ItemInstallQueue import com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING import com.android.launcher3.model.ModelUtils.WIDGET_FILTER @@ -24,6 +23,7 @@ import com.android.launcher3.model.ModelUtils.currentScreenContentFilter import com.android.launcher3.model.StringCache import com.android.launcher3.model.data.AppInfo import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.PredictedContainerInfo import com.android.launcher3.popup.PopupContainerWithArrow import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.Executors @@ -219,6 +219,10 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { val itemsToRebind = workspace.updateContainerItems(updates, launcher) PopupContainerWithArrow.dismissInvalidPopup(launcher) + updates + .mapNotNull { if (it is PredictedContainerInfo) it else null } + .forEach { launcher.bindPredictedContainerInfo(it) } + if (itemsToRebind.isEmpty()) return workspace.removeItemsByMatcher(ItemInfoMatcher.ofItems(itemsToRebind), false) itemsToRebind @@ -399,8 +403,6 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { @AnyThread override fun bindCompleteModelAsync( itemIdMap: IntSparseArrayMap, - extraItems: List, - stringCache: StringCache, isBindingSync: Boolean, ) { val taskTracker = CancellationSignal() @@ -455,7 +457,6 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { val currentScreenIds = getPagesToBindSynchronously(orderedScreenIds) fun setupPendingBind(pendingExecutor: Executor) { - executeCallbacksTask(pendingExecutor) { launcher.bindStringCache(stringCache) } executeCallbacksTask(pendingExecutor) { finishBindingItems(currentScreenIds) } pendingExecutor.execute { ItemInstallQueue.INSTANCE[launcher].resumeModelPush(FLAG_LOADER_RUNNING) @@ -499,9 +500,10 @@ class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, MAIN_EXECUTOR) bindItemsInChunks(currentAppWidgets, 1, MAIN_EXECUTOR) } - extraItems.forEach { - it?.let { executeCallbacksTask { launcher.bindExtraContainerItems(it) } } - } + + itemIdMap + .mapNotNull { if (it is PredictedContainerInfo) it else null } + .forEach { executeCallbacksTask { launcher.bindPredictedContainerInfo(it) } } val pendingTasks = RunnableList() val pendingExecutor = Executor { pendingTasks.add(it) } diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index 630c1fa14f..955e799559 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -85,7 +85,6 @@ import com.android.launcher3.dagger.StaticObjectModule; import com.android.launcher3.dagger.WindowManagerProxyModule; import com.android.launcher3.model.BaseLauncherBinder.BaseLauncherBinderFactory; import com.android.launcher3.model.BgDataModel; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.LayoutParserFactory; import com.android.launcher3.model.LayoutParserFactory.XmlLayoutParserFactory; import com.android.launcher3.model.LoaderTask.LoaderTaskFactory; @@ -111,7 +110,6 @@ import dagger.Component; import java.io.File; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -480,11 +478,7 @@ public class LauncherPreviewRenderer extends BaseContext } private void populateHotseatPredictions(BgDataModel dataModel) { - FixedContainerItems hotseatPredictions = - dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION); - List predictions = hotseatPredictions == null - ? Collections.emptyList() : hotseatPredictions.items; - + List predictions = dataModel.getPredictedContents(CONTAINER_HOTSEAT_PREDICTION); int predictionIndex = 0; for (int rank = 0; rank < mDp.numShownHotseatIcons; rank++) { if (predictions.size() <= predictionIndex) continue; diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java index aa70f65ad2..70819a4fc2 100644 --- a/src/com/android/launcher3/model/BaseLauncherBinder.java +++ b/src/com/android/launcher3/model/BaseLauncherBinder.java @@ -106,7 +106,12 @@ public class BaseLauncherBinder { } for (Callbacks cb : mCallbacksList) { - cb.bindCompleteModelAsync(itemsIdMap, extraItems, stringCache, isBindSync); + cb.bindCompleteModelAsync(itemsIdMap, isBindSync); + } + + executeCallbacksTask(c -> c.bindStringCache(stringCache), mUiExecutor); + for (FixedContainerItems extraItem: extraItems) { + executeCallbacksTask(c -> c.bindExtraContainerItems(extraItem), mUiExecutor); } } finally { Trace.endSection(); diff --git a/src/com/android/launcher3/model/BgDataModel.kt b/src/com/android/launcher3/model/BgDataModel.kt index f14c6c5eae..c57caf0e16 100644 --- a/src/com/android/launcher3/model/BgDataModel.kt +++ b/src/com/android/launcher3/model/BgDataModel.kt @@ -37,6 +37,7 @@ import com.android.launcher3.logging.FileLog import com.android.launcher3.model.data.AppInfo import com.android.launcher3.model.data.CollectionInfo import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.PredictedContainerInfo import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.model.repository.HomeScreenRepository import com.android.launcher3.model.repository.HomeScreenRepository.WorkspaceData.ChangeEvent.AddEvent @@ -55,7 +56,6 @@ import com.android.launcher3.util.PackageUserKey import com.android.launcher3.widget.model.WidgetsListBaseEntry import java.io.PrintWriter import java.util.Collections -import java.util.function.Consumer import java.util.function.Predicate import javax.inject.Inject import javax.inject.Provider @@ -77,13 +77,15 @@ constructor( lifeCycle: DaggerSingletonTracker, ) : LauncherDumpable { /** - * Map of all the ItemInfos (shortcuts, folders, and widgets) created by LauncherModel to their - * ids + * Map of all the ItemInfos (shortcuts, folders, widgets and predicted items) created by + * LauncherModel to their ids */ @JvmField val itemsIdMap = IntSparseArrayMap() /** Extra container based items */ - @JvmField val extraItems = IntSparseArrayMap() + @Deprecated("Use independent repository for each extra item") + @JvmField + val extraItems = IntSparseArrayMap() /** Maps all launcher activities to counts of their shortcuts. */ @JvmField val deepShortcutMap = HashMap() @@ -103,6 +105,14 @@ constructor( lifeCycle.addCloseable(dumpManager.register(this)) } + /** + * Returns the predicted items for the provided [containerId] or an empty list id no such + * container exists + */ + fun getPredictedContents(containerId: Int): List = + itemsIdMap[containerId].let { if (it is PredictedContainerInfo) it.getContents() else null } + ?: Collections.emptyList() + /** Clears all the data */ @Synchronized fun clear() { @@ -249,8 +259,10 @@ constructor( // Collect all model shortcuts val allWorkspaceItems = mutableListOf().apply { - forAllWorkspaceItemInfos(user) { + updateAndCollectWorkspaceItemInfos(user) { if (it.itemType == ITEM_TYPE_DEEP_SHORTCUT) add(ShortcutKey.fromItemInfo(it)) + // We don't care about the returned list + false } } allWorkspaceItems.addAll( @@ -339,19 +351,30 @@ constructor( } /** - * Calls the provided `op` for all workspaceItems in the in-memory model (both persisted items - * and dynamic/predicted items for the provided `userHandle`. Note the call is not synchronized - * over the model, that should be handled by the called. + * Calls the [op] for all workspaceItems in the in-memory model (both persisted items and + * dynamic/predicted items for the [userHandle]) and returns a list of updates to be dispatched + * to the callbacks. Note the call is not synchronized over the model, that should be handled by + * the called. */ - fun forAllWorkspaceItemInfos(userHandle: UserHandle, op: Consumer) { - itemsIdMap.forEach { if (it is WorkspaceItemInfo && userHandle == it.user) op.accept(it) } - - extraItems.forEach { info -> - info.items.forEach { - if (it is WorkspaceItemInfo && userHandle == it.user) op.accept(it) + fun updateAndCollectWorkspaceItemInfos( + userHandle: UserHandle, + op: (WorkspaceItemInfo) -> Boolean, + ): MutableList = + itemsIdMap.filterTo(mutableListOf()) { + when { + it is WorkspaceItemInfo && userHandle == it.user -> op.invoke(it) + it is PredictedContainerInfo -> { + // Do not use filter or any as we want to run the update on every item. If any + // single item was updated, we add the container to the list of updates + it.getContents().count { predictedItem -> + predictedItem is WorkspaceItemInfo && + predictedItem.user == userHandle && + op.invoke(predictedItem) + } > 0 + } + else -> false } } - } /** An object containing items corresponding to a fixed container */ class FixedContainerItems(@JvmField val containerId: Int, items: List) { @@ -368,23 +391,11 @@ constructor( * the client to move the executor to appropriate thread */ @AnyThread - fun bindCompleteModelAsync( - itemIdMap: IntSparseArrayMap, - extraItems: List, - stringCache: StringCache, - isBindingSync: Boolean, - ) { - Executors.MAIN_EXECUTOR.execute { - bindCompleteModel(itemIdMap, extraItems, stringCache, isBindingSync) - } + fun bindCompleteModelAsync(itemIdMap: IntSparseArrayMap, isBindingSync: Boolean) { + Executors.MAIN_EXECUTOR.execute { bindCompleteModel(itemIdMap, isBindingSync) } } - fun bindCompleteModel( - itemIdMap: IntSparseArrayMap, - extraItems: List, - stringCache: StringCache, - isBindingSync: Boolean, - ) {} + fun bindCompleteModel(itemIdMap: IntSparseArrayMap, isBindingSync: Boolean) {} fun bindItemsAdded(items: List<@JvmSuppressWildcards ItemInfo>) {} diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java index f740b49694..6b9217ce99 100644 --- a/src/com/android/launcher3/model/CacheDataUpdatedTask.java +++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java @@ -30,8 +30,8 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import java.util.ArrayList; import java.util.HashSet; +import java.util.List; /** * Handles changes due to cache updates. @@ -60,17 +60,18 @@ public class CacheDataUpdatedTask implements ModelUpdateTask { public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel, @NonNull AllAppsList apps) { IconCache iconCache = taskController.getIconCache(); - ArrayList updatedItems = new ArrayList<>(); + List updatedItems; synchronized (dataModel) { - dataModel.forAllWorkspaceItemInfos(mUser, si -> { + updatedItems = dataModel.updateAndCollectWorkspaceItemInfos(mUser, si -> { ComponentName cn = si.getTargetComponent(); if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && isValidShortcut(si) && cn != null && mPackages.contains(cn.getPackageName())) { iconCache.getTitleAndIcon(si, si.getMatchingLookupFlag()); - updatedItems.add(si); + return true; } + return false; }); dataModel.itemsIdMap.stream() diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt index 545fd5473b..6eedb73050 100644 --- a/src/com/android/launcher3/model/ModelTaskController.kt +++ b/src/com/android/launcher3/model/ModelTaskController.kt @@ -27,7 +27,6 @@ import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.PackageUserKey import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder -import java.util.Objects import java.util.function.Predicate import javax.inject.Inject @@ -63,17 +62,7 @@ constructor( if (workspaceUpdates.isNotEmpty()) { scheduleCallbackTask { it.bindItemsUpdated(workspaceUpdates) } } - // TODO: Probably duplicate call, verify and remove? dataModel.updateItems(allUpdates.toList(), null) - - // Bind extra items if any - allUpdates - .stream() - .mapToInt { it.container } - .distinct() - .mapToObj { dataModel.extraItems.get(it) } - .filter { Objects.nonNull(it) } - .forEach { bindExtraContainerItems(it) } } fun bindExtraContainerItems(item: FixedContainerItems) { diff --git a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java index f924a9f9c9..3ea8dda524 100644 --- a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java @@ -21,11 +21,10 @@ import androidx.annotation.NonNull; import com.android.launcher3.LauncherModel.ModelUpdateTask; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; -import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.PackageInstallInfo; -import java.util.ArrayList; import java.util.List; /** @@ -69,14 +68,15 @@ public class PackageIncrementalDownloadUpdatedTask implements ModelUpdateTask { taskController.bindApplicationsIfNeeded(); } - final ArrayList updatedWorkspaceItems = new ArrayList<>(); + final List updatedWorkspaceItems; synchronized (dataModel) { - dataModel.forAllWorkspaceItemInfos(mUser, si -> { + updatedWorkspaceItems = dataModel.updateAndCollectWorkspaceItemInfos(mUser, si -> { if (mPackageName.equals(si.getTargetPackage())) { si.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; si.setProgressLevel(downloadInfo); - updatedWorkspaceItems.add(si); + return true; } + return false; }); } taskController.bindUpdatedWorkspaceItems(updatedWorkspaceItems); diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java index a3561eda4d..0a10ea48c6 100644 --- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java +++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java @@ -30,7 +30,6 @@ import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.pm.PackageInstallInfo; import com.android.launcher3.util.InstantAppResolver; -import java.util.HashSet; import java.util.List; /** @@ -78,13 +77,14 @@ public class PackageInstallStateChangedTask implements ModelUpdateTask { } synchronized (dataModel) { - final HashSet updates = new HashSet<>(); - dataModel.forAllWorkspaceItemInfos(mInstallInfo.user, si -> { - if (si.hasPromiseIconUi() - && mInstallInfo.packageName.equals(si.getTargetPackage())) { - si.setProgressLevel(mInstallInfo); - updates.add(si); - } + final List updates = dataModel.updateAndCollectWorkspaceItemInfos( + mInstallInfo.user, si -> { + if (si.hasPromiseIconUi() + && mInstallInfo.packageName.equals(si.getTargetPackage())) { + si.setProgressLevel(mInstallInfo); + return true; + } + return false; }); dataModel.itemsIdMap.stream() diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 04f3faa194..98d287b083 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -57,7 +57,6 @@ import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.SafeCloseable; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -212,12 +211,12 @@ public class PackageUpdatedTask implements ModelUpdateTask { // Update shortcut infos if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) { - final ArrayList updatedWorkspaceItems = new ArrayList<>(); + final List updatedItems; // For system apps, package manager send OP_UPDATE when an app is enabled. final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE; synchronized (dataModel) { - dataModel.forAllWorkspaceItemInfos(mUser, itemInfo -> { + updatedItems = dataModel.updateAndCollectWorkspaceItemInfos(mUser, itemInfo -> { boolean infoUpdated = false; boolean shortcutUpdated = false; @@ -229,7 +228,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { if (itemInfo.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) { forceKeepShortcuts.add(itemInfo.id); if (mOp == OP_REMOVE) { - return; + return false; } } @@ -286,7 +285,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { + ", status=" + itemInfo.status + ", isArchived=" + itemInfo.isArchived()); } - return; + return false; } } else if (!isTargetValid) { removedShortcuts.add(itemInfo.id); @@ -297,7 +296,7 @@ public class PackageUpdatedTask implements ModelUpdateTask { + " package=" + itemInfo.getTargetPackage() + " status=" + itemInfo.status); } - return; + return false; } else { itemInfo.status = WorkspaceItemInfo.DEFAULT; infoUpdated = true; @@ -347,12 +346,10 @@ public class PackageUpdatedTask implements ModelUpdateTask { } } - if (infoUpdated || shortcutUpdated) { - updatedWorkspaceItems.add(itemInfo); - } if (infoUpdated && itemInfo.id != ItemInfo.NO_ID) { taskController.getModelWriter().updateItemInDatabase(itemInfo); } + return infoUpdated || shortcutUpdated; }); dataModel.itemsIdMap.stream() @@ -371,12 +368,12 @@ public class PackageUpdatedTask implements ModelUpdateTask { // activity, it will be marked as 'restored' during bind. widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; widgetInfo.installProgress = 100; - updatedWorkspaceItems.add(widgetInfo); + updatedItems.add(widgetInfo); taskController.getModelWriter().updateItemInDatabase(widgetInfo); }); } - taskController.bindUpdatedWorkspaceItems(updatedWorkspaceItems); + taskController.bindUpdatedWorkspaceItems(updatedItems); if (!removedShortcuts.isEmpty()) { taskController.deleteAndBindComponentsRemoved( ItemInfoMatcher.ofItemIds(removedShortcuts), diff --git a/src/com/android/launcher3/model/SessionFailureTask.kt b/src/com/android/launcher3/model/SessionFailureTask.kt index 6ed5178ed7..f1d328b663 100644 --- a/src/com/android/launcher3/model/SessionFailureTask.kt +++ b/src/com/android/launcher3/model/SessionFailureTask.kt @@ -38,21 +38,20 @@ class SessionFailureTask(val packageName: String, val user: UserHandle) : ModelU ApplicationInfoWrapper(taskController.context, packageName, user).isArchived() synchronized(dataModel) { if (isAppArchived) { - val updatedItems = mutableListOf() // Remove package icon cache entry for archived app in case of a session // failure. iconCache.remove( ComponentName(packageName, packageName + BaseIconCache.EMPTY_CLASS_NAME), user, ) - for (info in dataModel.itemsIdMap) { - if (info is WorkspaceItemInfo && info.isArchived && user == info.user) { - // Refresh icons on the workspace for archived apps. - iconCache.getTitleAndIcon(info, info.matchingLookupFlag) - updatedItems.add(info) + val updatedItems = + dataModel.updateAndCollectWorkspaceItemInfos(user) { info -> + if (info.isArchived) { + // Refresh icons on the workspace for archived apps. + iconCache.getTitleAndIcon(info, info.matchingLookupFlag) + true + } else false } - } - if (updatedItems.isNotEmpty()) { taskController.bindUpdatedWorkspaceItems(updatedItems) } diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.kt b/src/com/android/launcher3/model/ShortcutsChangedTask.kt index d6759e2d21..7ce4cdeb5b 100644 --- a/src/com/android/launcher3/model/ShortcutsChangedTask.kt +++ b/src/com/android/launcher3/model/ShortcutsChangedTask.kt @@ -21,6 +21,7 @@ import com.android.launcher3.Flags import com.android.launcher3.LauncherModel.ModelUpdateTask import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT import com.android.launcher3.icons.CacheableShortcutInfo +import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.shortcuts.ShortcutKey import com.android.launcher3.shortcuts.ShortcutRequest @@ -41,21 +42,22 @@ class ShortcutsChangedTask( apps: AllAppsList, ) { val context = taskController.context - // Find WorkspaceItemInfo's that have changed on the workspace. - val matchingWorkspaceItems = ArrayList() + val itemFilter: (WorkspaceItemInfo) -> Boolean = { + it.itemType == ITEM_TYPE_DEEP_SHORTCUT && packageName == it.targetPackage + } + // Find WorkspaceItemInfo's that have changed on the workspace. + val matchingShortcutIds = mutableSetOf() synchronized(dataModel) { - dataModel.forAllWorkspaceItemInfos(user) { wai: WorkspaceItemInfo -> - if ( - (wai.itemType == ITEM_TYPE_DEEP_SHORTCUT) && - packageName == wai.getIntent().getPackage() - ) { - matchingWorkspaceItems.add(wai) - } + dataModel.updateAndCollectWorkspaceItemInfos(user) { + if (itemFilter.invoke(it)) matchingShortcutIds.add(it.deepShortcutId) + + // We don't care about the returned list + false } } - if (matchingWorkspaceItems.isNotEmpty()) { + if (matchingShortcutIds.isNotEmpty()) { val infoWrapper = ApplicationInfoWrapper(context, packageName, user) if (shortcuts.isEmpty()) { // Verify that the app is indeed installed. @@ -68,30 +70,33 @@ class ShortcutsChangedTask( } } // Update the workspace to reflect the changes to updated shortcuts residing on it. - val allLauncherKnownIds = - matchingWorkspaceItems.map { item -> item.deepShortcutId }.distinct() - val shortcuts: List = + val pinnedShortcuts: Map = ShortcutRequest(context, user) - .forPackage(packageName, allLauncherKnownIds) + .forPackage(packageName, matchingShortcutIds.filterNotNullTo(mutableListOf())) .query(ShortcutRequest.ALL) + .associateBy { it.id } + val nonPinnedIds = matchingShortcutIds.toMutableSet() + val updatedWorkspaceItemInfos: List + synchronized(dataModel) { + updatedWorkspaceItemInfos = + dataModel.updateAndCollectWorkspaceItemInfos(user) { + if (!itemFilter.invoke(it)) return@updateAndCollectWorkspaceItemInfos false + val shortcutId = + it.deepShortcutId ?: return@updateAndCollectWorkspaceItemInfos false + val fullDetails = + pinnedShortcuts[shortcutId] + ?: return@updateAndCollectWorkspaceItemInfos false - val nonPinnedIds: MutableSet = HashSet(allLauncherKnownIds) - val updatedWorkspaceItemInfos = ArrayList() - for (fullDetails in shortcuts) { - if (!fullDetails.isPinned && !Flags.restoreArchivedShortcuts()) { - continue - } - val shortcutId = fullDetails.id - nonPinnedIds.remove(shortcutId) - matchingWorkspaceItems - .filter { itemInfo: WorkspaceItemInfo -> shortcutId == itemInfo.deepShortcutId } - .forEach { workspaceItemInfo: WorkspaceItemInfo -> - workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context) + if (!fullDetails.isPinned && !Flags.restoreArchivedShortcuts()) + return@updateAndCollectWorkspaceItemInfos false + + nonPinnedIds.remove(shortcutId) + it.updateFromDeepShortcutInfo(fullDetails, context) taskController.iconCache.getShortcutIcon( - workspaceItemInfo, + it, CacheableShortcutInfo(fullDetails, infoWrapper), ) - updatedWorkspaceItemInfos.add(workspaceItemInfo) + true } } @@ -99,9 +104,7 @@ class ShortcutsChangedTask( if (nonPinnedIds.isNotEmpty()) { taskController.deleteAndBindComponentsRemoved( ItemInfoMatcher.ofShortcutKeys( - nonPinnedIds - .map { id: String? -> ShortcutKey(packageName, user, id) } - .toSet() + nonPinnedIds.mapTo(mutableSetOf()) { ShortcutKey(packageName, user, it) } ), "removed because the shortcut is no longer available in shortcut service", ) diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java index 4d28ccb42d..1d41b8c403 100644 --- a/src/com/android/launcher3/model/UserLockStateChangedTask.java +++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java @@ -25,17 +25,17 @@ import androidx.annotation.NonNull; import com.android.launcher3.LauncherModel.ModelUpdateTask; import com.android.launcher3.LauncherSettings; -import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.shortcuts.ShortcutRequest; import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.ItemInfoMatcher; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; /** * Task to handle changing of lock state of the user @@ -73,11 +73,11 @@ public class UserLockStateChangedTask implements ModelUpdateTask { } // Update the workspace to reflect the changes to updated shortcuts residing on it. - ArrayList updatedWorkspaceItemInfos = new ArrayList<>(); + List updatedItemInfos; HashSet removedKeys = new HashSet<>(); synchronized (dataModel) { - dataModel.forAllWorkspaceItemInfos(mUser, si -> { + updatedItemInfos = dataModel.updateAndCollectWorkspaceItemInfos(mUser, si -> { if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { if (mIsUserUnlocked) { ShortcutKey key = ShortcutKey.fromItemInfo(si); @@ -86,7 +86,7 @@ public class UserLockStateChangedTask implements ModelUpdateTask { // (probably due to clear data), delete the workspace item as well if (shortcut == null) { removedKeys.add(key); - return; + return false; } si.runtimeStatusFlags &= ~FLAG_DISABLED_LOCKED_USER; si.updateFromDeepShortcutInfo(shortcut, context); @@ -94,11 +94,12 @@ public class UserLockStateChangedTask implements ModelUpdateTask { } else { si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER; } - updatedWorkspaceItemInfos.add(si); + return true; } + return false; }); } - taskController.bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos); + taskController.bindUpdatedWorkspaceItems(updatedItemInfos); if (!removedKeys.isEmpty()) { taskController.deleteAndBindComponentsRemoved( ItemInfoMatcher.ofShortcutKeys(removedKeys), diff --git a/src/com/android/launcher3/model/data/PredictedContainerInfo.kt b/src/com/android/launcher3/model/data/PredictedContainerInfo.kt new file mode 100644 index 0000000000..3728a34617 --- /dev/null +++ b/src/com/android/launcher3/model/data/PredictedContainerInfo.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 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.util.Log +import com.android.launcher3.util.ContentWriter + +/** Info representing a set of predicted items belonging to a particular container */ +class PredictedContainerInfo(containerId: Int, private val items: List) : + CollectionInfo() { + + init { + id = containerId + container = containerId + } + + override fun add(item: ItemInfo) { + Log.e("PredictedContainerInfo", "Trying to add $item to immutable prediction container") + } + + override fun getContents(): List = items + + override fun getAppContents(): List = + items.mapNotNull { if (it is WorkspaceItemInfo) it else null } + + override fun onAddToDatabase(writer: ContentWriter) = + throw RuntimeException("Persisting predicted items not supported") + + override fun dumpProperties() = "${super.dumpProperties()}, items: [${items.joinToString()}]" +} diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index 0c54d49420..7ca211ae55 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.secondarydisplay; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION; import static com.android.launcher3.util.WallpaperThemeManager.setWallpaperDependentTheme; import android.animation.Animator; @@ -39,7 +40,6 @@ import com.android.launcher3.DropTarget; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; -import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.AllAppsStore; @@ -53,10 +53,12 @@ import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.touch.ItemClickHandler.ItemClickProxy; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.IntSparseArrayMap; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Themes; @@ -64,6 +66,7 @@ import com.android.launcher3.views.BaseDragLayer; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * Launcher activity for secondary displays @@ -250,9 +253,20 @@ public class SecondaryDisplayLauncher extends BaseActivity } @Override - public void bindExtraContainerItems(BgDataModel.FixedContainerItems item) { - if (item.containerId == LauncherSettings.Favorites.CONTAINER_ALL_APPS_PREDICTION) { - mSecondaryDisplayQuickstepDelegate.setPredictedApps(item); + public void bindCompleteModel( + @NonNull IntSparseArrayMap itemIdMap, boolean isBindingSync) { + if (itemIdMap.get(CONTAINER_ALL_APPS_PREDICTION) instanceof PredictedContainerInfo pci) { + mSecondaryDisplayQuickstepDelegate.setPredictedApps(pci); + } + } + + @Override + public void bindItemsUpdated(@NonNull Set updates) { + for (ItemInfo updatedItem: updates) { + if (updatedItem.container == CONTAINER_ALL_APPS_PREDICTION + && updatedItem instanceof PredictedContainerInfo pci) { + mSecondaryDisplayQuickstepDelegate.setPredictedApps(pci); + } } } diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegate.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegate.java index fcd839abd7..34865ef91a 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegate.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayQuickstepDelegate.java @@ -18,7 +18,7 @@ package com.android.launcher3.secondarydisplay; import android.content.Context; import com.android.launcher3.R; -import com.android.launcher3.model.BgDataModel; +import com.android.launcher3.model.data.PredictedContainerInfo; import com.android.launcher3.util.ResourceBasedOverride; /** @@ -43,7 +43,7 @@ public class SecondaryDisplayQuickstepDelegate implements ResourceBasedOverride /** * Set predicted apps in top of app drawer. */ - public void setPredictedApps(BgDataModel.FixedContainerItems item) { + public void setPredictedApps(PredictedContainerInfo item) { } boolean enableTaskbarConnectedDisplays() { diff --git a/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java index d18fd288e9..2658b58440 100644 --- a/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -23,6 +23,7 @@ import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; import static com.android.launcher3.util.ModelTestExtensions.getBgDataModel; +import static com.android.launcher3.util.ModelTestExtensions.nonPredictedItemCount; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; import static org.junit.Assert.assertEquals; @@ -100,7 +101,8 @@ public class CacheDataUpdatedTaskTest { .build(); mLayoutProvider.setupDefaultLayoutProvider(builder); ModelTestExtensions.INSTANCE.loadModelSync(getModel()); - assertEquals(10, getBgDataModel(getModel()).itemsIdMap.size()); + // Items on homescreen and folders: + assertEquals(10, nonPredictedItemCount(getBgDataModel(getModel()).itemsIdMap)); } private CacheDataUpdatedTask newTask(int op, String... pkg) { diff --git a/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java index 7c4b361717..6d7a7b54ba 100644 --- a/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java @@ -21,6 +21,7 @@ import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; import static com.android.launcher3.util.ModelTestExtensions.getBgDataModel; +import static com.android.launcher3.util.ModelTestExtensions.nonPredictedItemCount; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; import static org.junit.Assert.assertEquals; @@ -84,7 +85,7 @@ public class PackageInstallStateChangedTaskTest { mDownloadingApps = IntSet.wrap(4, 5, 6, 7, 8, 9, 10); mLayoutProvider.setupDefaultLayoutProvider(builder); ModelTestExtensions.INSTANCE.loadModelSync(getModel()); - assertEquals(10, getBgDataModel(getModel()).itemsIdMap.size()); + assertEquals(10, nonPredictedItemCount(getBgDataModel(getModel()).itemsIdMap)); } private PackageInstallStateChangedTask newTask(String pkg, int progress) { @@ -128,6 +129,7 @@ public class PackageInstallStateChangedTaskTest { private void verifyProgressUpdate(int progress, int... idsUpdated) { IntSet updates = IntSet.wrap(idsUpdated); for (ItemInfo info : getBgDataModel(getModel()).itemsIdMap) { + if (info.id < 0) continue; int expectedProgress = updates.contains(info.id) ? progress : (mDownloadingApps.contains(info.id) ? 0 : 100); if (info instanceof WorkspaceItemInfo wi) { diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt index a76060a91a..7f4f4c8d10 100644 --- a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt +++ b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt @@ -13,6 +13,7 @@ import com.android.launcher3.LauncherSettings.Favorites.CELLX import com.android.launcher3.LauncherSettings.Favorites.CELLY import com.android.launcher3.LauncherSettings.Favorites.CONTAINER import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT import com.android.launcher3.LauncherSettings.Favorites.INTENT import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION @@ -25,6 +26,7 @@ import com.android.launcher3.LauncherSettings.Favorites.TITLE import com.android.launcher3.LauncherSettings.Favorites._ID import com.android.launcher3.model.BgDataModel import com.android.launcher3.model.ModelDbController +import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.util.Executors.MODEL_EXECUTOR import java.io.BufferedReader import java.io.InputStreamReader @@ -115,6 +117,10 @@ object ModelTestExtensions { return data!! } + /** Total number of items belonging to a non-predicted container */ + @JvmStatic + fun Iterable.nonPredictedItemCount() = count { it.container >= CONTAINER_HOTSEAT } + /** Creates an in-memory sqlite DB and initializes with the data in [insertFile] */ fun createInMemoryDb(insertFile: String): SQLiteDatabase = SQLiteDatabase.createInMemory(SQLiteDatabase.OpenParams.Builder().build()).also { db -> diff --git a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java index d64049d384..0c7d590bbb 100644 --- a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java +++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java @@ -16,6 +16,7 @@ package com.android.launcher3.model; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; +import static com.android.launcher3.util.ModelTestExtensions.nonPredictedItemCount; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -32,7 +33,6 @@ import androidx.test.filters.SmallTest; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.model.BgDataModel.Callbacks; -import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.Executors; @@ -176,9 +176,8 @@ public class ModelMultiCallbacksTest { MyCallbacks() { } @Override - public void bindCompleteModel(IntSparseArrayMap itemIdMap, - List extraItems, StringCache stringCache, - boolean isBindingSync) { + public void bindCompleteModel( + IntSparseArrayMap itemIdMap, boolean isBindingSync) { mItems = itemIdMap.stream().toList(); } @@ -195,7 +194,7 @@ public class ModelMultiCallbacksTest { public void verifyItemsBound(int totalItems) { assertNotNull(mItems); - assertEquals(mItems.size(), totalItems); + assertEquals(totalItems, nonPredictedItemCount(mItems)); } public Set allApps() {