Merge "Moving LauncherModel initialization to a separate class" into main
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index c2d6df5..870e6d6 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -85,6 +85,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
@@ -252,7 +253,7 @@
 
     public Point defaultWallpaperSize;
 
-    private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+    private final List<OnIDPChangeListener> mChangeListeners = new CopyOnWriteArrayList<>();
 
     @Inject
     InvariantDeviceProfile(
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 71013c3..bf2ad92 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,54 +16,32 @@
 
 package com.android.launcher3;
 
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
-import static android.content.Context.RECEIVER_EXPORTED;
-
-import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
-import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
-
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
-import androidx.core.os.BuildCompat;
 
+import com.android.launcher3.dagger.LauncherComponentProvider;
 import com.android.launcher3.graphics.ThemeManager;
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIconProvider;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.ModelLauncherCallbacks;
+import com.android.launcher3.model.ModelInitializer;
 import com.android.launcher3.model.WidgetsFilterDataProvider;
-import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 public class LauncherAppState implements SafeCloseable {
 
     public static final String TAG = "LauncherAppState";
-    public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -94,82 +72,21 @@
 
         mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
                 () -> context.getPackageManager().isSafeMode());
-        mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> {
-            if (modelPropertiesChanged) {
-                refreshAndReloadLauncher();
-            }
-        });
 
-        ThemeChangeListener themeChangeListener = this::refreshAndReloadLauncher;
-        ThemeManager.INSTANCE.get(context).addChangeListener(themeChangeListener);
-        mOnTerminateCallback.add(() ->
-                ThemeManager.INSTANCE.get(context).removeChangeListener(themeChangeListener));
-
-        ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
-        LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
-        launcherApps.registerCallback(callbacks);
-        mOnTerminateCallback.add(() ->
-                mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
-
-        if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
-            ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
-            params.setEnableUnarchivalConfirmation(false);
-            params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps());
-            launcherApps.setArchiveCompatibility(params);
-        }
-
-        SimpleBroadcastReceiver modelChangeReceiver =
-                new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
-        modelChangeReceiver.register(
-                ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
-        if (BuildConfig.IS_STUDIO_BUILD) {
-            modelChangeReceiver.register(RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD);
-        }
-        mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely());
-
-        SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
-                .addUserEventListener(mModel::onUserEvent);
-        mOnTerminateCallback.add(userChangeListener::close);
-
-        if (enableSmartspaceRemovalToggle()) {
-            OnSharedPreferenceChangeListener firstPagePinnedItemListener =
-                    new OnSharedPreferenceChangeListener() {
-                        @Override
-                        public void onSharedPreferenceChanged(
-                                SharedPreferences sharedPreferences, String key) {
-                            if (SMARTSPACE_ON_HOME_SCREEN.equals(key)) {
-                                mModel.forceReload();
-                            }
-                        }
-                    };
-            LauncherPrefs.getPrefs(mContext).registerOnSharedPreferenceChangeListener(
-                    firstPagePinnedItemListener);
-            mOnTerminateCallback.add(() -> LauncherPrefs.getPrefs(mContext)
-                    .unregisterOnSharedPreferenceChangeListener(firstPagePinnedItemListener));
-        }
-
-        LockedUserState.get(context).runOnUserUnlocked(() -> {
-            CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
-            mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
-
-            SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
-                    mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
-            mOnTerminateCallback.add(iconChangeTracker::close);
-
-            InstallSessionTracker installSessionTracker =
-                    InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(callbacks);
-            mOnTerminateCallback.add(installSessionTracker::unregister);
-        });
-
-        // Register an observer to rebind the notification listener when dots are re-enabled.
-        SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
-        SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged;
-        settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister);
-        onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
-        mOnTerminateCallback.add(() ->
-                settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
-        // Register an observer to notify Launcher about Private Space settings toggle.
-        registerPrivateSpaceHideWhenLockListener(settingsCache);
+        ModelInitializer initializer = new ModelInitializer(
+                context,
+                LauncherComponentProvider.get(context).getIconPool(),
+                mIconCache,
+                mInvariantDeviceProfile,
+                ThemeManager.INSTANCE.get(context),
+                UserCache.INSTANCE.get(context),
+                SettingsCache.INSTANCE.get(context),
+                mIconProvider,
+                CustomWidgetManager.INSTANCE.get(context),
+                InstallSessionHelper.INSTANCE.get(context),
+                closeable -> mOnTerminateCallback.add(closeable::close)
+        );
+        initializer.initialize(mModel);
     }
 
     public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -186,32 +103,6 @@
         mOnTerminateCallback.add(mModel::destroy);
     }
 
-    private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
-        if (areNotificationDotsEnabled) {
-            NotificationListener.requestRebind(new ComponentName(
-                    mContext, NotificationListener.class));
-        }
-    }
-
-    private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) {
-        SettingsCache.OnChangeListener psHideWhenLockChangedListener =
-                this::onPrivateSpaceHideWhenLockChanged;
-        settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psHideWhenLockChangedListener);
-        mOnTerminateCallback.add(() -> settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI,
-                psHideWhenLockChangedListener));
-    }
-
-    private void onPrivateSpaceHideWhenLockChanged(boolean isPrivateSpaceHideOnLockEnabled) {
-        mModel.forceReload();
-    }
-
-    private void refreshAndReloadLauncher() {
-        LauncherIcons.clearPool(mContext);
-        mIconCache.updateIconParams(
-                mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
-        mModel.forceReload();
-    }
-
     /**
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index 185629b..6e4276d 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -15,13 +15,11 @@
  */
 package com.android.launcher3
 
-import android.app.admin.DevicePolicyManager
 import android.content.Context
 import android.content.Intent
 import android.content.pm.ShortcutInfo
 import android.os.UserHandle
 import android.text.TextUtils
-import android.util.Log
 import android.util.Pair
 import androidx.annotation.WorkerThread
 import com.android.launcher3.celllayout.CellPosMapper
@@ -47,7 +45,6 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.shortcuts.ShortcutRequest
-import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.PackageManagerHelper
@@ -173,15 +170,8 @@
         }
     }
 
-    fun onBroadcastIntent(intent: Intent) {
-        if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
-        when (intent.action) {
-            LauncherAppState.ACTION_FORCE_ROLOAD ->
-                // If we have changed locale we need to clear out the labels in all apps/workspace.
-                forceReload()
-            DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED ->
-                enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
-        }
+    fun reloadStringCache() {
+        enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
     }
 
     /**
diff --git a/src/com/android/launcher3/model/ModelInitializer.kt b/src/com/android/launcher3/model/ModelInitializer.kt
new file mode 100644
index 0000000..69a320a
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelInitializer.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED
+import android.content.ComponentName
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.ArchiveCompatibilityParams
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener
+import com.android.launcher3.LauncherModel
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.graphics.ThemeManager
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.LauncherIconProvider
+import com.android.launcher3.icons.LauncherIcons.IconPool
+import com.android.launcher3.notification.NotificationListener
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI
+import com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI
+import com.android.launcher3.util.SimpleBroadcastReceiver
+import com.android.launcher3.widget.custom.CustomWidgetManager
+import java.util.function.Consumer
+
+/** Utility class for initializing all model callbacks */
+class ModelInitializer(
+    @ApplicationContext private val context: Context,
+    private val iconPool: IconPool,
+    private val iconCache: IconCache,
+    private val idp: InvariantDeviceProfile,
+    private val themeManager: ThemeManager,
+    private val userCache: UserCache,
+    private val settingsCache: SettingsCache,
+    private val iconProvider: LauncherIconProvider,
+    private val customWidgetManager: CustomWidgetManager,
+    private val installSessionHelper: InstallSessionHelper,
+    private val closeActions: Consumer<SafeCloseable>,
+) {
+
+    fun initialize(model: LauncherModel) {
+        fun refreshAndReloadLauncher() {
+            iconPool.clear()
+            iconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize)
+            model.forceReload()
+        }
+
+        // IDP changes
+        val idpChangeListener = OnIDPChangeListener { modelChanged ->
+            if (modelChanged) refreshAndReloadLauncher()
+        }
+        idp.addOnChangeListener(idpChangeListener)
+        closeActions.accept { idp.removeOnChangeListener(idpChangeListener) }
+
+        // Theme changes
+        val themeChangeListener = ThemeChangeListener { refreshAndReloadLauncher() }
+        themeManager.addChangeListener(themeChangeListener)
+        closeActions.accept { themeManager.removeChangeListener(themeChangeListener) }
+
+        // System changes
+        val modelCallbacks = model.newModelCallbacks()
+        val launcherApps = context.getSystemService(LauncherApps::class.java)!!
+        launcherApps.registerCallback(modelCallbacks)
+        closeActions.accept { launcherApps.unregisterCallback(modelCallbacks) }
+
+        if (Utilities.ATLEAST_V && Flags.enableSupportForArchiving()) {
+            launcherApps.setArchiveCompatibility(
+                ArchiveCompatibilityParams().apply {
+                    setEnableUnarchivalConfirmation(false)
+                    setEnableIconOverlay(!Flags.useNewIconForArchivedApps())
+                }
+            )
+        }
+
+        // Device profile policy changes
+        val dpUpdateReceiver =
+            SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.reloadStringCache() }
+        dpUpdateReceiver.register(ACTION_DEVICE_POLICY_RESOURCE_UPDATED)
+        closeActions.accept { dpUpdateReceiver.unregisterReceiverSafely() }
+
+        // Development helper
+        if (BuildConfig.IS_STUDIO_BUILD) {
+            val reloadReceiver =
+                SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.forceReload() }
+            reloadReceiver.register(Context.RECEIVER_EXPORTED, ACTION_FORCE_RELOAD)
+            closeActions.accept { reloadReceiver.unregisterReceiverSafely() }
+        }
+
+        // User changes
+        closeActions.accept(userCache.addUserEventListener(model::onUserEvent))
+
+        // Private space settings changes
+        val psSettingsListener = SettingsCache.OnChangeListener { model.forceReload() }
+        settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+        closeActions.accept {
+            settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+        }
+
+        // Notification dots changes
+        val notificationChanges =
+            SettingsCache.OnChangeListener { dotsEnabled ->
+                if (dotsEnabled)
+                    NotificationListener.requestRebind(
+                        ComponentName(context, NotificationListener::class.java)
+                    )
+            }
+        settingsCache.register(NOTIFICATION_BADGING_URI, notificationChanges)
+        notificationChanges.onSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI))
+        closeActions.accept {
+            settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationChanges)
+        }
+
+        // removable smartspace
+        if (Flags.enableSmartspaceRemovalToggle()) {
+            val smartSpacePrefChanges =
+                SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+                    if (LoaderTask.SMARTSPACE_ON_HOME_SCREEN == key) model.forceReload()
+                }
+            getPrefs(context).registerOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+            closeActions.accept {
+                getPrefs(context).unregisterOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+            }
+        }
+
+        // Custom widgets
+        closeActions.accept(customWidgetManager.addWidgetRefreshCallback(model::rebindCallbacks))
+
+        // Icon changes
+        closeActions.accept(
+            iconProvider.registerIconChangeListener(model::onAppIconChanged, MODEL_EXECUTOR.handler)
+        )
+
+        // Install session changes
+        closeActions.accept(installSessionHelper.registerInstallTracker(modelCallbacks))
+    }
+
+    companion object {
+        private const val ACTION_FORCE_RELOAD = "force-reload-launcher"
+    }
+}
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index b9c928c..7451ce2 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -34,13 +34,15 @@
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SafeCloseable;
 
 import java.lang.ref.WeakReference;
 import java.util.Objects;
 
 @SuppressWarnings("NewApi")
 @WorkerThread
-public class InstallSessionTracker extends PackageInstaller.SessionCallback {
+public class InstallSessionTracker extends PackageInstaller.SessionCallback implements
+        SafeCloseable {
 
     public static final String TAG = "InstallSessionTracker";
 
@@ -196,7 +198,8 @@
         }
     }
 
-    public void unregister() {
+    @Override
+    public void close() {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
             mInstaller.unregisterSessionCallback(this);
         } else {
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
index 15a9964..23c1da9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -219,7 +219,7 @@
             .whenever(launcherApps)
             .unregisterPackageInstallerSessionCallback(installSessionTracker)
         // When
-        installSessionTracker.unregister()
+        installSessionTracker.close()
         // Then
         verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
     }