From 040ff3a7bfd22ee7a9f3f4fc9801b738660d539d Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 24 Mar 2025 09:49:07 -0700 Subject: [PATCH] Adding HomeScreenRepo to expose homescreen data as repository Bug: 390572144 Test: Presubmit Flag: com.android.launcher3.model_repository Change-Id: If82a388a44d7a9c448f6eb4901c2acdb508ccfdc --- aconfig/launcher.aconfig | 7 ++ .../model/QuickstepModelDelegateTest.kt | 2 +- .../android/launcher3/model/BgDataModel.java | 43 ++++++- .../android/launcher3/model/LoaderTask.java | 2 + .../launcher3/model/ModelTaskController.kt | 2 + .../android/launcher3/model/ModelWriter.java | 15 ++- .../model/repository/HomeScreenRepository.kt | 113 ++++++++++++++++++ .../model/ShortcutsChangedTaskTest.kt | 2 +- 8 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/com/android/launcher3/model/repository/HomeScreenRepository.kt diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index a24c199724..2e96104781 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -680,3 +680,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "model_repository" + namespace: "launcher" + description: "Adds various data repositories for the model" + bug: "390572144" +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt index b5953c7834..6cf049fb4d 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt @@ -65,7 +65,7 @@ class QuickstepModelDelegateTest { underTest.mHotseatState.predictor = hotseatPredictor underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor underTest.mModel = LauncherAppState.getInstance(context).model - underTest.mDataModel = BgDataModel(WidgetsModel(context)) + underTest.mDataModel = BgDataModel(WidgetsModel(context), { null }) } @Test diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index 3e5f7dc7e0..ea15e344b6 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -44,6 +44,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.BuildConfig; +import com.android.launcher3.Flags; import com.android.launcher3.Workspace; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dagger.LauncherAppSingleton; @@ -52,6 +53,10 @@ 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.WorkspaceItemInfo; +import com.android.launcher3.model.repository.HomeScreenRepository; +import com.android.launcher3.model.repository.HomeScreenRepository.WorkspaceData.ChangeEvent.AddEvent; +import com.android.launcher3.model.repository.HomeScreenRepository.WorkspaceData.ChangeEvent.RemoveEvent; +import com.android.launcher3.model.repository.HomeScreenRepository.WorkspaceData.ChangeEvent.UpdateEvent; import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.shortcuts.ShortcutRequest; @@ -69,6 +74,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -81,6 +87,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Inject; +import javax.inject.Provider; /** * All the data stored in-memory and managed by the LauncherModel @@ -119,6 +126,9 @@ public class BgDataModel { */ public final StringCache stringCache = new StringCache(); + @Nullable + private final HomeScreenRepository mRepo; + /** * Id when the model was last bound */ @@ -132,8 +142,9 @@ public class BgDataModel { && !enableSmartspaceRemovalToggle(); @Inject - public BgDataModel(WidgetsModel widgetsModel) { + public BgDataModel(WidgetsModel widgetsModel, Provider homeDataProvider) { this.widgetsModel = widgetsModel; + mRepo = Flags.modelRepository() ? homeDataProvider.get() : null; } /** @@ -188,13 +199,19 @@ public class BgDataModel { removeItem(context, Arrays.asList(items)); } - public synchronized void removeItem(Context context, List items) { + public synchronized void removeItem(Context context, Collection items) { + removeItem(context, items, null); + } + + public synchronized void removeItem(Context context, Collection items, + @Nullable Object owner) { if (BuildConfig.IS_STUDIO_BUILD) { items.stream() .filter(item -> item.itemType == ITEM_TYPE_FOLDER || item.itemType == ITEM_TYPE_APP_PAIR) .forEach(item -> itemsIdMap.stream() .filter(info -> info.container == item.id) + .filter(info -> !items.contains(info)) // We are deleting a collection which still contains items that // think they are contained by that collection. .forEach(info -> Log.e(TAG, @@ -205,13 +222,25 @@ public class BgDataModel { items.forEach(item -> itemsIdMap.remove(item.id)); items.stream().map(info -> info.user).distinct().forEach( user -> updateShortcutPinnedState(context, user)); + if (Flags.modelRepository() && mRepo != null) { + mRepo.dispatchChange(this, new RemoveEvent(new ArrayList<>(items), owner)); + } } public synchronized void addItem(Context context, ItemInfo item, boolean newItem) { + addItem(context, item, newItem, null); + } + + public synchronized void addItem(Context context, ItemInfo item, boolean newItem, + @Nullable Object owner) { itemsIdMap.put(item.id, item); if (newItem && item.itemType == ITEM_TYPE_DEEP_SHORTCUT) { updateShortcutPinnedState(context, item.user); } + if (Flags.modelRepository() && mRepo != null) { + mRepo.dispatchChange(this, new AddEvent(Collections.singletonList(item), owner)); + } + if (BuildConfig.IS_DEBUG_DEVICE && newItem && item.container != CONTAINER_DESKTOP @@ -222,6 +251,16 @@ public class BgDataModel { } } + public synchronized void updateItems(List items, @Nullable Object owner) { + if (Flags.modelRepository() && mRepo != null) { + mRepo.dispatchChange(this, new UpdateEvent(items, owner)); + } + } + + public synchronized void dataLoadComplete() { + if (Flags.modelRepository() && mRepo != null) mRepo.onNewBind(this); + } + /** * Updates the deep shortcuts state in system to match out internal model, pinning any missing * shortcuts and unpinning any extra shortcuts. diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index aa812535a3..26f9503dee 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -528,6 +528,8 @@ public class LoaderTask implements Runnable { processAppPairItems(); c.commitRestoredItems(); + + mBgDataModel.dataLoadComplete(); } } diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt index f17ca32cce..545fd5473b 100644 --- a/src/com/android/launcher3/model/ModelTaskController.kt +++ b/src/com/android/launcher3/model/ModelTaskController.kt @@ -63,6 +63,8 @@ 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 diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java index 2650e03c63..db3ad8a91a 100644 --- a/src/com/android/launcher3/model/ModelWriter.java +++ b/src/com/android/launcher3/model/ModelWriter.java @@ -255,7 +255,7 @@ public class ModelWriter { mModel.getModelDbController().insert(writer.getValues(mContext)); synchronized (mBgDataModel) { checkItemInfoLocked(item.id, item, stackTrace); - mBgDataModel.addItem(mContext, item, true); + mBgDataModel.addItem(mContext, item, true, mOwner); verifier.verifyModel(); } }).executeOnModelThread(); @@ -292,9 +292,9 @@ public class ModelWriter { enqueueDeleteRunnable(newModelTask(() -> { for (ItemInfo item : items) { mModel.getModelDbController().delete(itemIdMatch(item.id), null); - mBgDataModel.removeItem(mContext, item); - verifier.verifyModel(); } + mBgDataModel.removeItem(mContext, items, mOwner); + verifier.verifyModel(); })); } @@ -308,12 +308,13 @@ public class ModelWriter { enqueueDeleteRunnable(newModelTask(() -> { mModel.getModelDbController().delete( Favorites.CONTAINER + "=" + info.id, null); - mBgDataModel.removeItem(mContext, info.getContents()); - info.getContents().clear(); mModel.getModelDbController().delete( Favorites._ID + "=" + info.id, null); - mBgDataModel.removeItem(mContext, info); + + List itemsToDelete = new ArrayList<>(info.getContents()); + itemsToDelete.add(info); + mBgDataModel.removeItem(mContext, itemsToDelete, mOwner); verifier.verifyModel(); })); } @@ -412,6 +413,7 @@ public class ModelWriter { mModel.getModelDbController().update( mWriter.get().getValues(mContext), itemIdMatch(mItemId), null); updateItemArrays(mItem, mItemId); + mBgDataModel.updateItems(Collections.singletonList(mItem), mOwner); } } @@ -436,6 +438,7 @@ public class ModelWriter { updateItemArrays(item, itemId); } t.commit(); + mBgDataModel.updateItems(mItems, mOwner); } catch (Exception e) { e.printStackTrace(); } diff --git a/src/com/android/launcher3/model/repository/HomeScreenRepository.kt b/src/com/android/launcher3/model/repository/HomeScreenRepository.kt new file mode 100644 index 0000000000..625fe9fc9c --- /dev/null +++ b/src/com/android/launcher3/model/repository/HomeScreenRepository.kt @@ -0,0 +1,113 @@ +/* + * 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.repository + +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.model.BgDataModel +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.repository.HomeScreenRepository.WorkspaceData.ChangeEvent +import com.android.launcher3.util.Executors +import com.android.launcher3.util.IntSparseArrayMap +import java.util.concurrent.CopyOnWriteArrayList +import java.util.function.Consumer +import javax.inject.Inject +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.callbackFlow + +/** + * Repository for the home screen data. + * + * This class is responsible for holding the current state of the home screen and providing a way to + * listen for changes to the data. + */ +@LauncherAppSingleton +class HomeScreenRepository @Inject constructor() { + + /** + * Represents the current home screen data model. There are two ways this can change: + * 1) The model can be replaced completely with a new data, which can be observed using the + * state flow. + * 2) Changes can be made to the existing data, the diff can be observed using + * [WorkspaceData.updates] + */ + val workspaceStateFlow = MutableStateFlow(WorkspaceData(IntSparseArrayMap())) + + class WorkspaceData(var itemsIdMap: IntSparseArrayMap) { + + val scope = MainScope() + + internal val updateListeners = CopyOnWriteArrayList>() + + val updates = + callbackFlow { + val listener = Consumer { trySend(it) } + updateListeners.add(listener) + awaitClose { updateListeners.remove(listener) } + } + + /** Represents a change being made to the existing workspace data */ + sealed interface ChangeEvent { + // The items being changed + val items: List + + // The source of the change. If its user driven, it will point to the UI component where + // the user is interacting or null if the change was made as a result of some system + // event. Clients can use this to exclude self-made changes. + val owner: Any? + + /** New items were added to the model */ + data class AddEvent(override val items: List, override val owner: Any?) : + ChangeEvent + + /** Some properties of existing items changed */ + data class UpdateEvent(override val items: List, override val owner: Any?) : + ChangeEvent + + /** Some items were removed from the model */ + data class RemoveEvent(override val items: List, override val owner: Any?) : + ChangeEvent + } + } + + /** + * Used to notify that the model data was completely replaced. This is only meant to be used by + * the model, clients should just rely on the events provided by the StateFlow + */ + fun onNewBind(model: BgDataModel) { + val items = model.itemsIdMap.clone() + + Executors.MAIN_EXECUTOR.execute { + workspaceStateFlow.value.scope.cancel() + workspaceStateFlow.value = WorkspaceData(items) + } + } + + /** + * Used to notify a particular change to the workspace data. This is only meant to be used by + * the model, clients should just rely on the events provided by the StateFlow + */ + fun dispatchChange(model: BgDataModel, event: ChangeEvent) { + val items = model.itemsIdMap.clone() + Executors.MAIN_EXECUTOR.execute { + workspaceStateFlow.value.itemsIdMap = items + workspaceStateFlow.value.updateListeners.forEach { it.accept(event) } + } + } +} diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt index 8fd042878d..2ab87c402a 100644 --- a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt @@ -71,7 +71,7 @@ class ShortcutsChangedTaskTest { private val mockTaskController: ModelTaskController = mock() private val mockAllApps: AllAppsList = mock() private val mockIconCache: IconCache = mock() - private val bgDataModel = BgDataModel(mock()) + private val bgDataModel = BgDataModel(mock(), mock()) private val expectedWai = WorkspaceItemInfo().apply {