OSDN Git Service

Adding an asynchronous search interface for apps search
authorSunny Goyal <sunnygoyal@google.com>
Fri, 29 May 2015 21:54:40 +0000 (14:54 -0700)
committerSunny Goyal <sunnygoyal@google.com>
Mon, 1 Jun 2015 22:58:29 +0000 (15:58 -0700)
Change-Id: Ib09df0a3d587dc60ed888ddbd0edf058e4a1cc3e

src/com/android/launcher3/Launcher.java
src/com/android/launcher3/allapps/AllAppsContainerView.java
src/com/android/launcher3/allapps/AlphabeticalAppsList.java
src/com/android/launcher3/allapps/AppSearchManager.java [new file with mode: 0644]
src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java [new file with mode: 0644]
src/com/android/launcher3/model/AbstractUserComparator.java [new file with mode: 0644]
src/com/android/launcher3/model/AppNameComparator.java

index 51f0916..ef34660 100644 (file)
@@ -100,6 +100,7 @@ import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.PagedView.PageSwitchListener;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AppSearchManager;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
@@ -583,6 +584,11 @@ public class Launcher extends Activity
                     }
                 }
             }
+
+            @Override
+            public void setSearchManager(AppSearchManager manager) {
+                mAppsView.setSearchManager(manager);
+            }
         });
         mLauncherCallbacks.setLauncherSearchCallback(new Launcher.LauncherSearchCallbacks() {
             private boolean mImportanceStored = false;
@@ -1158,6 +1164,11 @@ public class Launcher extends Activity
          * Called to dismiss all apps if it is showing.
          */
         public void dismissAllApps();
+
+        /**
+         * Sets the search manager to be used for app search.
+         */
+        public void setSearchManager(AppSearchManager manager);
     }
 
     public interface LauncherSearchCallbacks {
index d81f97f..c05f7c0 100644 (file)
@@ -41,6 +41,7 @@ import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 import android.widget.TextView;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BaseContainerView;
 import com.android.launcher3.BubbleTextView;
@@ -54,15 +55,15 @@ import com.android.launcher3.Folder;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherTransitionable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.allapps.AppSearchManager.AppSearchResultCallback;
 import com.android.launcher3.util.Thunk;
 
+import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Pattern;
 
 
 /**
@@ -171,7 +172,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable,
         AlphabeticalAppsList.AdapterChangedCallback, AllAppsGridAdapter.PredictionBarSpacerCallbacks,
         View.OnTouchListener, View.OnClickListener, View.OnLongClickListener,
-        ViewTreeObserver.OnPreDrawListener {
+        ViewTreeObserver.OnPreDrawListener, AppSearchResultCallback {
 
     public static final boolean GRID_MERGE_SECTIONS = true;
 
@@ -183,8 +184,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     private static final int FADE_OUT_DURATION = 100;
     private static final int SEARCH_TRANSLATION_X_DP = 18;
 
-    private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
-
     @Thunk Launcher mLauncher;
     @Thunk AlphabeticalAppsList mApps;
     private LayoutInflater mLayoutInflater;
@@ -221,6 +220,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     private CheckLongPressHelper mPredictionIconCheckForLongPress;
     private View mPredictionIconUnderTouch;
 
+    private AppSearchManager mSearchManager;
+
     public AllAppsContainerView(Context context) {
         this(context, null);
     }
@@ -231,7 +232,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
 
     public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        LauncherAppState app = LauncherAppState.getInstance();
         Resources res = context.getResources();
 
         mLauncher = (Launcher) context;
@@ -258,6 +258,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         mContentMarginStart = mAdapter.getContentMarginStart();
 
         mApps.setAdapter(mAdapter);
+        mSearchManager = mApps.newSimpleAppSearchManager();
     }
 
     /**
@@ -281,6 +282,11 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
         mApps.addApps(apps);
     }
 
+    public void setSearchManager(AppSearchManager searchManager) {
+        mSearchManager.cancel(true);
+        mSearchManager = searchManager;
+    }
+
     /**
      * Updates existing apps in the list
      */
@@ -664,45 +670,26 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
     public void afterTextChanged(final Editable s) {
         String queryText = s.toString();
         if (queryText.isEmpty()) {
-            mApps.setFilter(null);
+            mSearchManager.cancel(true);
+            mApps.setOrderedFilter(null);
         } else {
             String formatStr = getResources().getString(R.string.all_apps_no_search_results);
             mAdapter.setEmptySearchText(String.format(formatStr, queryText));
 
-            // Do an intersection of the words in the query and each title, and filter out all the
-            // apps that don't match all of the words in the query.
-            final String queryTextLower = queryText.toLowerCase();
-            final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
-            mApps.setFilter(new AlphabeticalAppsList.Filter() {
-                @Override
-                public boolean retainApp(AppInfo info, String sectionName) {
-                    if (sectionName.toLowerCase().contains(queryTextLower)) {
-                        return true;
-                    }
-                    String title = info.title.toString();
-                    String[] words = SPLIT_PATTERN.split(title.toLowerCase());
-                    for (int qi = 0; qi < queryWords.length; qi++) {
-                        boolean foundMatch = false;
-                        for (int i = 0; i < words.length; i++) {
-                            if (words[i].startsWith(queryWords[qi])) {
-                                foundMatch = true;
-                                break;
-                            }
-                        }
-                        if (!foundMatch) {
-                            // If there is a word in the query that does not match any words in this
-                            // title, so skip it.
-                            return false;
-                        }
-                    }
-                    return true;
-                }
-            });
+            mSearchManager.cancel(false);
+            mSearchManager.doSearch(queryText, this);
         }
         scrollToTop();
     }
 
     @Override
+    public void onSearchResult(ArrayList<ComponentName> apps) {
+        if (apps != null) {
+            mApps.setOrderedFilter(apps);
+        }
+    }
+
+    @Override
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
         if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) {
             // Skip the quick-launch if there isn't exactly one item
@@ -796,7 +783,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      * recycler view.
      */
     private boolean handleTouchEvent(MotionEvent ev) {
-        LauncherAppState app = LauncherAppState.getInstance();
         DeviceProfile grid = mLauncher.getDeviceProfile();
         int x = (int) ev.getX();
         int y = (int) ev.getY();
@@ -919,6 +905,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      * Shows the search field.
      */
     private void showSearchField() {
+        mSearchManager.connect();
+
         // Show the search bar and focus the search
         final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
                 getContext().getResources().getDisplayMetrics());
@@ -949,6 +937,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
      * Hides the search field.
      */
     private void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) {
+        mSearchManager.cancel(true);
+
         final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0;
         final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
                 getContext().getResources().getDisplayMetrics());
@@ -966,7 +956,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
                             if (resetTextField) {
                                 mSearchBarEditView.setText("");
                             }
-                            mApps.setFilter(null);
+                            mApps.setOrderedFilter(null);
                             if (returnFocusToRecyclerView) {
                                 mAppsRecyclerView.requestFocus();
                             }
@@ -983,7 +973,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
             if (resetTextField) {
                 mSearchBarEditView.setText("");
             }
-            mApps.setFilter(null);
+            mApps.setOrderedFilter(null);
             mSearchButtonView.setAlpha(1f);
             mSearchButtonView.setTranslationX(0f);
             if (returnFocusToRecyclerView) {
index 13e1828..0dc2d1e 100644 (file)
@@ -1,13 +1,31 @@
+/*
+ * Copyright (C) 2015 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.allapps;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.model.AbstractUserComparator;
 import com.android.launcher3.model.AppNameComparator;
 
 import java.nio.charset.CharsetEncoder;
@@ -115,13 +133,6 @@ public class AlphabeticalAppsList {
     }
 
     /**
-     * A filter interface to limit the set of applications in the apps list.
-     */
-    public interface Filter {
-        boolean retainApp(AppInfo info, String sectionName);
-    }
-
-    /**
      * Callback to notify when the set of adapter items have changed.
      */
     public interface AdapterChangedCallback {
@@ -198,7 +209,7 @@ public class AlphabeticalAppsList {
     private Launcher mLauncher;
 
     // The set of apps from the system not including predictions
-    private List<AppInfo> mApps = new ArrayList<>();
+    private final List<AppInfo> mApps = new ArrayList<>();
     // The set of filtered apps with the current filter
     private List<AppInfo> mFilteredApps = new ArrayList<>();
     // The current set of adapter items
@@ -211,9 +222,10 @@ public class AlphabeticalAppsList {
     private List<ComponentName> mPredictedAppComponents = new ArrayList<>();
     // The set of predicted apps resolved from the component names and the current set of apps
     private List<AppInfo> mPredictedApps = new ArrayList<>();
+    // The of ordered component names as a result of a search query
+    private ArrayList<ComponentName> mSearchResults;
     private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
     private RecyclerView.Adapter mAdapter;
-    private Filter mFilter;
     private AlphabeticIndexCompat mIndexer;
     private AppNameComparator mAppNameComparator;
     private MergeAlgorithm mMergeAlgorithm;
@@ -235,6 +247,10 @@ public class AlphabeticalAppsList {
         mAdapterChangedCallback = cb;
     }
 
+    public SimpleAppSearchManagerImpl newSimpleAppSearchManager() {
+        return new SimpleAppSearchManagerImpl(mApps);
+    }
+
     /**
      * Sets the number of apps per row.  Used only for AppsContainerView.SECTIONED_GRID_COALESCED.
      */
@@ -293,22 +309,22 @@ public class AlphabeticalAppsList {
      * Returns whether there are is a filter set.
      */
     public boolean hasFilter() {
-        return (mFilter != null);
+        return (mSearchResults != null);
     }
 
     /**
      * Returns whether there are no filtered results.
      */
     public boolean hasNoFilteredResults() {
-        return (mFilter != null) && mFilteredApps.isEmpty();
+        return (mSearchResults != null) && mFilteredApps.isEmpty();
     }
 
     /**
-     * Sets the current filter for this list of apps.
+     * Sets the sorted list of filtered components.
      */
-    public void setFilter(Filter f) {
-        if (mFilter != f) {
-            mFilter = f;
+    public void setOrderedFilter(ArrayList<ComponentName> f) {
+        if (mSearchResults != f) {
+            mSearchResults = f;
             updateAdapterItems();
         }
     }
@@ -428,7 +444,9 @@ public class AlphabeticalAppsList {
             for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
                 allApps.addAll(entry.getValue());
             }
-            mApps = allApps;
+
+            mApps.clear();
+            mApps.addAll(allApps);
         } else {
             // Just compute the section headers for use below
             for (AppInfo info : mApps) {
@@ -483,16 +501,12 @@ public class AlphabeticalAppsList {
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
-        int numApps = mApps.size();
+        List<AppInfo> apps = getFiltersAppInfos();
+        int numApps = apps.size();
         for (int i = 0; i < numApps; i++) {
-            AppInfo info = mApps.get(i);
+            AppInfo info = apps.get(i);
             String sectionName = getAndUpdateCachedSectionName(info.title);
 
-            // Check if we want to retain this app
-            if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
-                continue;
-            }
-
             // Create a new section if the section names do not match
             if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
                 lastSectionName = sectionName;
@@ -533,6 +547,41 @@ public class AlphabeticalAppsList {
         }
     }
 
+    private List<AppInfo> getFiltersAppInfos() {
+        if (mSearchResults == null) {
+            return mApps;
+        }
+
+        int total = mSearchResults.size();
+        final HashMap<ComponentName, Integer> sortOrder = new HashMap<>(total);
+        for (int i = 0; i < total; i++) {
+            sortOrder.put(mSearchResults.get(i), i);
+        }
+
+        ArrayList<AppInfo> result = new ArrayList<>();
+        for (AppInfo info : mApps) {
+            if (sortOrder.containsKey(info.componentName)) {
+                result.add(info);
+            }
+        }
+
+        Collections.sort(result, new AbstractUserComparator<AppInfo>(
+                LauncherAppState.getInstance().getContext()) {
+
+            @Override
+            public int compare(AppInfo lhs, AppInfo rhs) {
+                Integer indexA = sortOrder.get(lhs.componentName);
+                int result = indexA.compareTo(sortOrder.get(rhs.componentName));
+                if (result == 0) {
+                    return super.compare(lhs, rhs);
+                } else {
+                    return result;
+                }
+            }
+        });
+        return result;
+    }
+
     /**
      * Merges multiple sections to reduce visual raggedness.
      */
diff --git a/src/com/android/launcher3/allapps/AppSearchManager.java b/src/com/android/launcher3/allapps/AppSearchManager.java
new file mode 100644 (file)
index 0000000..b6aa223
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 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.allapps;
+
+import android.content.ComponentName;
+
+import java.util.ArrayList;
+
+/**
+ * Interface for handling app search.
+ */
+public interface AppSearchManager {
+
+    /**
+     * Called when the search is about to be used. This method is optional for making a query but
+     * calling this appropriately can improve the initial response time.
+     */
+    void connect();
+
+    /**
+     * Cancels all pending search requests.
+     *
+     * @param interruptActiveRequests if true, any active requests which are already executing will
+     * be invalidated, and the corresponding results will not be sent. The client should usually
+     * set this to true, before beginning a new search session.
+     */
+    void cancel(boolean interruptActiveRequests);
+
+    /**
+     * Performs a search
+     */
+    void doSearch(String query, AppSearchResultCallback callback);
+
+    /**
+     * Callback for getting search results.
+     */
+    public interface AppSearchResultCallback {
+
+        /**
+         * Called when the search is complete.
+         *
+         * @param apps sorted list of matching components or null if in case of failure.
+         */
+        void onSearchResult(ArrayList<ComponentName> apps);
+    }
+}
diff --git a/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java b/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java
new file mode 100644 (file)
index 0000000..e8a31b5
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015 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.allapps;
+
+import android.content.ComponentName;
+import android.os.Handler;
+
+import com.android.launcher3.AppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link AppSearchManager} which does label matching on the UI thread.
+ */
+public class SimpleAppSearchManagerImpl implements AppSearchManager {
+
+    private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
+
+    private final List<AppInfo> mApps;
+    private final Handler mResultHandler;
+
+    public SimpleAppSearchManagerImpl(List<AppInfo> apps) {
+        mApps = apps;
+        mResultHandler = new Handler();
+    }
+
+    @Override
+    public void connect() {
+        // No op
+    }
+
+    @Override
+    public void cancel(boolean interruptActiveRequests) {
+        if (interruptActiveRequests) {
+            mResultHandler.removeCallbacksAndMessages(null);
+        }
+    }
+
+    @Override
+    public void doSearch(String query, final AppSearchResultCallback callback) {
+        // Do an intersection of the words in the query and each title, and filter out all the
+        // apps that don't match all of the words in the query.
+        final String queryTextLower = query.toLowerCase();
+        final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
+        final ArrayList<ComponentName> result = new ArrayList<ComponentName>();
+        int total = mApps.size();
+
+        for (int i = 0; i < total; i++) {
+            AppInfo info = mApps.get(i);
+            if (!result.contains(info.componentName) && matches(info, queryWords)) {
+                result.add(info.componentName);
+            }
+        }
+        mResultHandler.post(new Runnable() {
+
+            @Override
+            public void run() {
+                callback.onSearchResult(result);
+            }
+        });
+    }
+
+    private boolean matches(AppInfo info, String[] queryWords) {
+        String title = info.title.toString();
+        String[] words = SPLIT_PATTERN.split(title.toLowerCase());
+        for (int qi = 0; qi < queryWords.length; qi++) {
+            boolean foundMatch = false;
+            for (int i = 0; i < words.length; i++) {
+                if (words[i].startsWith(queryWords[qi])) {
+                    foundMatch = true;
+                    break;
+                }
+            }
+            if (!foundMatch) {
+                // If there is a word in the query that does not match any words in this
+                // title, so skip it.
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
diff --git a/src/com/android/launcher3/model/AbstractUserComparator.java b/src/com/android/launcher3/model/AbstractUserComparator.java
new file mode 100644 (file)
index 0000000..cf47ce6
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 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.content.Context;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+
+import java.util.Comparator;
+import java.util.HashMap;
+
+/**
+ * A comparator to arrange items based on user profiles.
+ */
+public abstract class AbstractUserComparator<T extends ItemInfo> implements Comparator<T> {
+
+    private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
+    private final UserManagerCompat mUserManager;
+    private final UserHandleCompat mMyUser;
+
+    public AbstractUserComparator(Context context) {
+        mUserManager = UserManagerCompat.getInstance(context);
+        mMyUser = UserHandleCompat.myUserHandle();
+    }
+
+    @Override
+    public int compare(T lhs, T rhs) {
+        if (mMyUser.equals(lhs.user)) {
+            return -1;
+        } else {
+            Long aUserSerial = getAndCacheUserSerial(lhs.user);
+            Long bUserSerial = getAndCacheUserSerial(rhs.user);
+            return aUserSerial.compareTo(bUserSerial);
+        }
+    }
+
+    /**
+     * Returns the user serial for this user, using a cached serial if possible.
+     */
+    private Long getAndCacheUserSerial(UserHandleCompat user) {
+        Long userSerial = mUserSerialCache.get(user);
+        if (userSerial == null) {
+            userSerial = mUserManager.getSerialNumberForUser(user);
+            mUserSerialCache.put(user, userSerial);
+        }
+        return userSerial;
+    }
+
+    public void clearUserCache() {
+        mUserSerialCache.clear();
+    }
+}
index 706f751..cdac40a 100644 (file)
@@ -1,14 +1,27 @@
+/*
+ * Copyright (C) 2015 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.content.Context;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
-import com.android.launcher3.compat.UserHandleCompat;
-import com.android.launcher3.compat.UserManagerCompat;
+
 import java.text.Collator;
 import java.util.Comparator;
-import java.util.HashMap;
 
 /**
  * Class to manage access to an app name comparator.
@@ -16,17 +29,15 @@ import java.util.HashMap;
  * Used to sort application name in all apps view and widget tray view.
  */
 public class AppNameComparator {
-    private final UserManagerCompat mUserManager;
     private final Collator mCollator;
-    private final Comparator<ItemInfo> mAppInfoComparator;
+    private final AbstractUserComparator<ItemInfo> mAppInfoComparator;
     private final Comparator<String> mSectionNameComparator;
-    private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
 
     public AppNameComparator(Context context) {
         mCollator = Collator.getInstance();
-        mUserManager = UserManagerCompat.getInstance(context);
-        mAppInfoComparator = new Comparator<ItemInfo>() {
+        mAppInfoComparator = new AbstractUserComparator<ItemInfo>(context) {
 
+            @Override
             public final int compare(ItemInfo a, ItemInfo b) {
                 // Order by the title in the current locale
                 int result = compareTitles(a.title.toString(), b.title.toString());
@@ -38,13 +49,7 @@ public class AppNameComparator {
                     if (result == 0) {
                         // If the two apps are the same component, then prioritize by the order that
                         // the app user was created (prioritizing the main user's apps)
-                        if (UserHandleCompat.myUserHandle().equals(a.user)) {
-                            return -1;
-                        } else {
-                            Long aUserSerial = getAndCacheUserSerial(a.user);
-                            Long bUserSerial = getAndCacheUserSerial(b.user);
-                            return aUserSerial.compareTo(bUserSerial);
-                        }
+                        return super.compare(a, b);
                     }
                 }
                 return result;
@@ -63,7 +68,7 @@ public class AppNameComparator {
      */
     public Comparator<ItemInfo> getAppInfoComparator() {
         // Clear the user serial cache so that we get serials as needed in the comparator
-        mUserSerialCache.clear();
+        mAppInfoComparator.clearUserCache();
         return mAppInfoComparator;
     }
 
@@ -90,16 +95,4 @@ public class AppNameComparator {
         // Order by the title in the current locale
         return mCollator.compare(titleA, titleB);
     }
-
-    /**
-     * Returns the user serial for this user, using a cached serial if possible.
-     */
-    private Long getAndCacheUserSerial(UserHandleCompat user) {
-        Long userSerial = mUserSerialCache.get(user);
-        if (userSerial == null) {
-            userSerial = mUserManager.getSerialNumberForUser(user);
-            mUserSerialCache.put(user, userSerial);
-        }
-        return userSerial;
-    }
 }