Making UserCache the source of truth for all user events

Bug: 243688989
Test: Presubmit
Flag: N/A
Change-Id: I0e6b853d965eff1abaeb3b26dd6b94424e5212df
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8071ae4..caf5755 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -183,7 +183,6 @@
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.pm.PinRequestHelper;
-import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
@@ -208,7 +207,6 @@
 import com.android.launcher3.util.PendingRequestArgs;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener;
 import com.android.launcher3.util.SystemUiController;
@@ -411,8 +409,6 @@
     protected long mLastTouchUpTime = -1;
     private boolean mTouchInProgress;
 
-    private SafeCloseable mUserChangedCallbackCloseable;
-
     // New InstanceId is assigned to mAllAppsSessionLogId for each AllApps sessions.
     // When Launcher is not in AllApps state mAllAppsSessionLogId will be null.
     // User actions within AllApps state are logged with this InstanceId, to recreate AllApps
@@ -581,9 +577,6 @@
         mRotationHelper.initialize();
         TraceHelper.INSTANCE.endSection();
 
-        mUserChangedCallbackCloseable = UserCache.INSTANCE.get(this).addUserChangeListener(
-                () -> getStateManager().goToState(NORMAL));
-
         if (Utilities.ATLEAST_R) {
             getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
         }
@@ -1804,7 +1797,6 @@
         LauncherAppState.getIDP(this).removeOnChangeListener(this);
 
         mOverlayManager.onActivityDestroyed(this);
-        mUserChangedCallbackCloseable.close();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -2974,9 +2966,14 @@
     public void bindAllApplications(AppInfo[] apps, int flags,
             Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
         Preconditions.assertUIThread();
+        boolean hadWorkApps = mAppsView.shouldShowTabs();
         AllAppsStore appsStore = mAppsView.getAppsStore();
         appsStore.setApps(apps, flags, packageUserKeytoUidMap);
         PopupContainerWithArrow.dismissInvalidPopup(this);
+        if (hadWorkApps != mAppsView.shouldShowTabs()) {
+            getStateManager().goToState(NORMAL);
+        }
+
         if (Utilities.ATLEAST_S) {
             Trace.endAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
                     DISPLAY_ALL_APPS_TRACE_COOKIE);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 4b7aeeb..eb1c4d4 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -111,10 +111,6 @@
         SimpleBroadcastReceiver modelChangeReceiver =
                 new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
         modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
-                Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
-                Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
-                Intent.ACTION_MANAGED_PROFILE_UNLOCKED,
-                Intent.ACTION_PROFILE_INACCESSIBLE,
                 ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
         if (FeatureFlags.IS_STUDIO_BUILD) {
             modelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
@@ -122,7 +118,7 @@
         mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver));
 
         SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
-                .addUserChangeListener(mModel::forceReload);
+                .addUserEventListener(mModel::onUserEvent);
         mOnTerminateCallback.add(userChangeListener::close);
 
         LockedUserState.get(context).runOnUserUnlocked(() -> {
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index f225f85..ddafd53 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -95,15 +95,6 @@
 
     static final String TAG = "Launcher.Model";
 
-    // Broadcast intent to track when the profile gets locked:
-    // ACTION_MANAGED_PROFILE_UNAVAILABLE can be used until Android U where profile no longer gets
-    // locked when paused.
-    // ACTION_PROFILE_INACCESSIBLE always means that the profile is getting locked but it only
-    // appeared in Android S.
-    private static final String ACTION_PROFILE_LOCKED = Utilities.ATLEAST_U
-            ? Intent.ACTION_PROFILE_INACCESSIBLE
-            : Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
-
     @NonNull
     private final LauncherAppState mApp;
     @NonNull
@@ -303,28 +294,6 @@
         if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
             // If we have changed locale we need to clear out the labels in all apps/workspace.
             forceReload();
-        } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
-                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)
-                || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)
-                || Intent.ACTION_PROFILE_INACCESSIBLE.equals(action)) {
-            UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORK_TAB_MISSING, "onBroadcastIntent intentAction: " + action +
-                        " user: " + user);
-            }
-            if (user != null) {
-                if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) ||
-                        Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
-                    enqueueModelUpdateTask(new PackageUpdatedTask(
-                            PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
-                }
-
-                if (ACTION_PROFILE_LOCKED.equals(action)
-                        || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) {
-                    enqueueModelUpdateTask(new UserLockStateChangedTask(
-                            user, Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)));
-                }
-            }
         } else if (ACTION_DEVICE_POLICY_RESOURCE_UPDATED.equals(action)) {
             enqueueModelUpdateTask(new ReloadStringCacheTask(mModelDelegate));
         } else if (IS_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
@@ -337,6 +306,33 @@
     }
 
     /**
+     * Called then there use a user event
+     * @see UserCache#addUserEventListener
+     */
+    public void onUserEvent(UserHandle user, String action) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.WORK_TAB_MISSING, "onBroadcastIntent intentAction: "
+                    + action + " user: " + user);
+        }
+
+        if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
+                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
+            enqueueModelUpdateTask(new PackageUpdatedTask(
+                    PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
+        }
+
+        if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
+                || UserCache.ACTION_PROFILE_UNLOCKED.equals(action)) {
+            enqueueModelUpdateTask(new UserLockStateChangedTask(
+                    user, UserCache.ACTION_PROFILE_UNLOCKED.equals(action)));
+        }
+        if (UserCache.ACTION_PROFILE_ADDED.equals(action)
+                || UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
+            forceReload();
+        }
+    }
+
+    /**
      * Reloads the workspace items from the DB and re-binds the workspace. This should generally
      * not be called as DB updates are automatically followed by UI update
      */
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 898009d..2b8b2c9 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -1100,7 +1100,10 @@
         }
     }
 
-    protected boolean shouldShowTabs() {
+    /**
+     * Returns true if the container has work apps.
+     */
+    public boolean shouldShowTabs() {
         return mHasWorkApps;
     }
 
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 24a9609..c313886 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -16,16 +16,21 @@
 
 package com.android.launcher3.pm;
 
+import static com.android.launcher3.Utilities.ATLEAST_U;
 import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
-import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
 import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
-import android.util.LongSparseArray;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
@@ -34,136 +39,123 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
 
 /**
  * Class which manages a local cache of user handles to avoid system rpc
  */
-public class UserCache {
+public class UserCache implements SafeCloseable {
+
+    public static final String ACTION_PROFILE_ADDED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_ADDED : Intent.ACTION_MANAGED_PROFILE_ADDED;
+    public static final String ACTION_PROFILE_REMOVED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_REMOVED : Intent.ACTION_MANAGED_PROFILE_REMOVED;
+
+    public static final String ACTION_PROFILE_UNLOCKED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_ACCESSIBLE : Intent.ACTION_MANAGED_PROFILE_UNLOCKED;
+    public static final String ACTION_PROFILE_LOCKED = ATLEAST_U
+            ? Intent.ACTION_PROFILE_INACCESSIBLE : Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
 
     public static final MainThreadInitializedObject<UserCache> INSTANCE =
             new MainThreadInitializedObject<>(UserCache::new);
 
-    private final Context mContext;
-    private final UserManager mUserManager;
-    private final ArrayList<Runnable> mUserChangeListeners = new ArrayList<>();
+    private final List<BiConsumer<UserHandle, String>> mUserEventListeners = new ArrayList<>();
     private final SimpleBroadcastReceiver mUserChangeReceiver =
             new SimpleBroadcastReceiver(this::onUsersChanged);
 
-    private LongSparseArray<UserHandle> mUsers;
-    // Create a separate reverse map as LongSparseArray.indexOfValue checks if objects are same
-    // and not {@link Object#equals}
-    private ArrayMap<UserHandle, Long> mUserToSerialMap;
+    private final Context mContext;
+
+    @NonNull
+    private Map<UserHandle, Long> mUserToSerialMap;
 
     private UserCache(Context context) {
         mContext = context;
-        mUserManager = context.getSystemService(UserManager.class);
+        mUserToSerialMap = Collections.emptyMap();
+        MODEL_EXECUTOR.execute(this::initAsync);
     }
 
+    @Override
+    public void close() {
+        MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafely(mContext));
+    }
+
+    @WorkerThread
+    private void initAsync() {
+        mUserChangeReceiver.register(mContext,
+                Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
+                Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
+                ACTION_PROFILE_ADDED,
+                ACTION_PROFILE_REMOVED,
+                ACTION_PROFILE_UNLOCKED,
+                ACTION_PROFILE_LOCKED);
+        updateCache();
+    }
+
+    @AnyThread
     private void onUsersChanged(Intent intent) {
         testLogD(WORK_TAB_MISSING, "onUsersChanged intent: " + intent);
-        enableAndResetCache();
-        mUserChangeListeners.forEach(Runnable::run);
+
+        MODEL_EXECUTOR.execute(this::updateCache);
+        UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
+        if (user == null) {
+            return;
+        }
+        String action = intent.getAction();
+        mUserEventListeners.forEach(l -> l.accept(user, action));
+    }
+
+    @WorkerThread
+    private void updateCache() {
+        mUserToSerialMap = queryAllUsers(mContext.getSystemService(UserManager.class));
     }
 
     /**
      * Adds a listener for user additions and removals
      */
-    public SafeCloseable addUserChangeListener(Runnable command) {
-        synchronized (this) {
-            if (mUserChangeListeners.isEmpty()) {
-                // Enable caching and start listening for user broadcast
-                mUserChangeReceiver.register(mContext,
-                        Intent.ACTION_MANAGED_PROFILE_ADDED,
-                        Intent.ACTION_MANAGED_PROFILE_REMOVED);
-                enableAndResetCache();
-            }
-            mUserChangeListeners.add(command);
-            return () -> removeUserChangeListener(command);
-        }
-    }
-
-    private void enableAndResetCache() {
-        synchronized (this) {
-            mUsers = new LongSparseArray<>();
-            mUserToSerialMap = new ArrayMap<>();
-            List<UserHandle> users = mUserManager.getUserProfiles();
-            if (users != null) {
-                for (UserHandle user : users) {
-                    testLogD(WORK_TAB_MISSING, "caching user: " + user);
-                    long serial = mUserManager.getSerialNumberForUser(user);
-                    mUsers.put(serial, user);
-                    mUserToSerialMap.put(user, serial);
-                }
-            }
-        }
-    }
-
-    private void removeUserChangeListener(Runnable command) {
-        synchronized (this) {
-            mUserChangeListeners.remove(command);
-            if (mUserChangeListeners.isEmpty()) {
-                // Disable cache and stop listening
-                mContext.unregisterReceiver(mUserChangeReceiver);
-
-                mUsers = null;
-                mUserToSerialMap = null;
-            }
-        }
+    public SafeCloseable addUserEventListener(BiConsumer<UserHandle, String> listener) {
+        mUserEventListeners.add(listener);
+        return () -> mUserEventListeners.remove(listener);
     }
 
     /**
      * @see UserManager#getSerialNumberForUser(UserHandle)
      */
     public long getSerialNumberForUser(UserHandle user) {
-        synchronized (this) {
-            if (mUserToSerialMap != null) {
-                Long serial = mUserToSerialMap.get(user);
-                return serial == null ? 0 : serial;
-            }
-        }
-        return mUserManager.getSerialNumberForUser(user);
+        Long serial = mUserToSerialMap.get(user);
+        return serial == null ? 0 : serial;
     }
 
     /**
      * @see UserManager#getUserForSerialNumber(long)
      */
     public UserHandle getUserForSerialNumber(long serialNumber) {
-        synchronized (this) {
-            if (mUsers != null) {
-                return mUsers.get(serialNumber);
-            }
-        }
-        return mUserManager.getUserForSerialNumber(serialNumber);
+        Long value = serialNumber;
+        return mUserToSerialMap
+                .entrySet()
+                .stream()
+                .filter(entry -> value.equals(entry.getValue()))
+                .findFirst()
+                .map(Map.Entry::getKey)
+                .orElse(Process.myUserHandle());
     }
 
     /**
      * @see UserManager#getUserProfiles()
      */
     public List<UserHandle> getUserProfiles() {
-        StringBuilder usersToReturn = new StringBuilder();
-        List<UserHandle> users;
-        if (sDebugTracing) {
-            users = mUserManager.getUserProfiles();
-            for (UserHandle u : users) {
-                usersToReturn.append(u).append(" && ");
-            }
-            testLogD(WORK_TAB_MISSING, "users from userManager: " + usersToReturn);
-        }
+        return List.copyOf(mUserToSerialMap.keySet());
+    }
 
-        synchronized (this) {
-            if (mUsers != null) {
-                usersToReturn = new StringBuilder();
-                for (UserHandle u : mUserToSerialMap.keySet()) {
-                    usersToReturn.append(u).append(" && ");
-                }
-                testLogD(WORK_TAB_MISSING, "users from cache: " + usersToReturn);
-                return new ArrayList<>(mUserToSerialMap.keySet());
-            } else {
-                testLogD(WORK_TAB_MISSING, "users from cache null");
+    private static Map<UserHandle, Long> queryAllUsers(UserManager userManager) {
+        Map<UserHandle, Long> users = new ArrayMap<>();
+        List<UserHandle> usersActual = userManager.getUserProfiles();
+        if (usersActual != null) {
+            for (UserHandle user : usersActual) {
+                long serial = userManager.getSerialNumberForUser(user);
+                users.put(user, serial);
             }
         }
-
-        users = mUserManager.getUserProfiles();
-        return users == null ? Collections.emptyList() : users;
+        return users;
     }
 }