Tweaking section processing for different languages

- Ensuring that apps with non-letter/digit characters are ordered last in the misc bucket
- Removing duplicate latin-alphabet sections for Simplified Chinese
- Adding more appropriate misc bucket label for Japanese

Bug 21022854

Change-Id: I62c7b219820ef88787fcfa83f1bd4202f16f9c0c
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java
index de4edcb..dc75637 100644
--- a/src/com/android/launcher3/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/AlphabeticalAppsList.java
@@ -14,23 +14,28 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
 
 
 /**
  * A private class to manage access to an app name comparator.
  */
 class AppNameComparator {
-    private UserManagerCompat mUserManager;
-    private Comparator<AppInfo> mAppNameComparator;
+    private final UserManagerCompat mUserManager;
+    private final Collator mCollator;
+    private final Comparator<AppInfo> mAppInfoComparator;
+    private final Comparator<String> mSectionNameComparator;
     private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
 
     public AppNameComparator(Context context) {
-        final Collator collator = Collator.getInstance();
+        mCollator = Collator.getInstance();
         mUserManager = UserManagerCompat.getInstance(context);
-        mAppNameComparator = new Comparator<AppInfo>() {
+        mAppInfoComparator = new Comparator<AppInfo>() {
             public final int compare(AppInfo a, AppInfo b) {
-                // Order by the title
-                int result = collator.compare(a.title.toString(), b.title.toString());
+                // Order by the title in the current locale
+                int result = compareTitles(a.title.toString(), b.title.toString());
                 if (result == 0) {
                     // If two apps have the same title, then order by the component name
                     result = a.componentName.compareTo(b.componentName);
@@ -49,15 +54,45 @@
                 return result;
             }
         };
+        mSectionNameComparator = new Comparator<String>() {
+            @Override
+            public int compare(String o1, String o2) {
+                return compareTitles(o1, o2);
+            }
+        };
     }
 
     /**
      * Returns a locale-aware comparator that will alphabetically order a list of applications.
      */
-    public Comparator<AppInfo> getComparator() {
+    public Comparator<AppInfo> getAppInfoComparator() {
         // Clear the user serial cache so that we get serials as needed in the comparator
         mUserSerialCache.clear();
-        return mAppNameComparator;
+        return mAppInfoComparator;
+    }
+
+    /**
+     * Returns a locale-aware comparator that will alphabetically order a list of section names.
+     */
+    public Comparator<String> getSectionNameComparator() {
+        return mSectionNameComparator;
+    }
+
+    /**
+     * Compares two titles with the same return value semantics as Comparator.
+     */
+    private int compareTitles(String titleA, String titleB) {
+        // Ensure that we de-prioritize any titles that don't start with a linguistic letter or digit
+        boolean aStartsWithLetter = Character.isLetterOrDigit(titleA.codePointAt(0));
+        boolean bStartsWithLetter = Character.isLetterOrDigit(titleB.codePointAt(0));
+        if (aStartsWithLetter && !bStartsWithLetter) {
+            return -1;
+        } else if (!aStartsWithLetter && bStartsWithLetter) {
+            return 1;
+        }
+
+        // Order by the title in the current locale
+        return mCollator.compare(titleA, titleB);
     }
 
     /**
@@ -219,6 +254,7 @@
     private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
     private static final int MAX_NUM_MERGES_PHONE = 2;
 
+    private Context mContext;
     private List<AppInfo> mApps = new ArrayList<>();
     private List<AppInfo> mFilteredApps = new ArrayList<>();
     private List<AdapterItem> mSectionedFilteredApps = new ArrayList<>();
@@ -234,6 +270,7 @@
     private int mNumAppsPerRow;
 
     public AlphabeticalAppsList(Context context, int numAppsPerRow) {
+        mContext = context;
         mIndexer = new AlphabeticIndexCompat(context);
         mAppNameComparator = new AppNameComparator(context);
         setNumAppsPerRow(numAppsPerRow);
@@ -400,7 +437,7 @@
      * Implementation to actually add an app to the alphabetic list, but does not notify.
      */
     private void addApp(AppInfo info) {
-        int index = Collections.binarySearch(mApps, info, mAppNameComparator.getComparator());
+        int index = Collections.binarySearch(mApps, info, mAppNameComparator.getAppInfoComparator());
         if (index < 0) {
             mApps.add(-(index + 1), info);
         }
@@ -411,7 +448,44 @@
      */
     private void onAppsUpdated() {
         // Sort the list of apps
-        Collections.sort(mApps, mAppNameComparator.getComparator());
+        Collections.sort(mApps, mAppNameComparator.getAppInfoComparator());
+
+        // As a special case for some languages (currently only Simplified Chinese), we may need to
+        // coalesce sections
+        Locale curLocale = mContext.getResources().getConfiguration().locale;
+        TreeMap<String, ArrayList<AppInfo>> sectionMap = null;
+        boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
+        if (localeRequiresSectionSorting) {
+            // Compute the section headers.  We use a TreeMap with the section name comparator to
+            // ensure that the sections are ordered when we iterate over it later
+            sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator());
+            for (AppInfo info : mApps) {
+                // Add the section to the cache
+                String sectionName = mCachedSectionNames.get(info.title);
+                if (sectionName == null) {
+                    sectionName = mIndexer.computeSectionName(info.title);
+                    mCachedSectionNames.put(info.title, sectionName);
+                }
+
+                // Add it to the mapping
+                ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
+                if (sectionApps == null) {
+                    sectionApps = new ArrayList<>();
+                    sectionMap.put(sectionName, sectionApps);
+                }
+                sectionApps.add(info);
+            }
+        } else {
+            // Just compute the section headers for use below
+            for (AppInfo info : mApps) {
+                // Add the section to the cache
+                String sectionName = mCachedSectionNames.get(info.title);
+                if (sectionName == null) {
+                    sectionName = mIndexer.computeSectionName(info.title);
+                    mCachedSectionNames.put(info.title, sectionName);
+                }
+            }
+        }
 
         // Prepare to update the list of sections, filtered apps, etc.
         mFilteredApps.clear();
@@ -444,23 +518,22 @@
         }
 
         // Add all the other apps to the combined list
-        allApps.addAll(mApps);
+        if (localeRequiresSectionSorting) {
+            for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
+                allApps.addAll(entry.getValue());
+            }
+        } else {
+            allApps.addAll(mApps);
+        }
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
-        // combined list
+        // ordered set of sections
         int numApps = allApps.size();
         for (int i = 0; i < numApps; i++) {
             boolean isPredictedApp = i < numPredictedApps;
             AppInfo info = allApps.get(i);
-            String sectionName = "";
-            if (!isPredictedApp) {
-                // Only cache section names from non-predicted apps
-                sectionName = mCachedSectionNames.get(info.title);
-                if (sectionName == null) {
-                    sectionName = mIndexer.computeSectionName(info.title);
-                    mCachedSectionNames.put(info.title, sectionName);
-                }
-            }
+            // The section name was computed above so this should be find
+            String sectionName = isPredictedApp ? "" : mCachedSectionNames.get(info.title);
 
             // Check if we want to retain this app
             if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
@@ -478,7 +551,7 @@
                 mFastScrollerSections.add(lastFastScrollerSectionInfo);
 
                 // Create a new section item to break the flow of items in the list
-                if (!AppsContainerView.GRID_HIDE_SECTION_HEADERS && !hasFilter()) {
+                if (!hasFilter()) {
                     AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo);
                     mSectionedFilteredApps.add(sectionItem);
                 }
diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java
index 5630449..a6902d5 100644
--- a/src/com/android/launcher3/AppsGridAdapter.java
+++ b/src/com/android/launcher3/AppsGridAdapter.java
@@ -63,11 +63,7 @@
 
             if (mApps.getAdapterItems().get(position).isSectionHeader) {
                 // Section break spans full width
-                if (AppsContainerView.GRID_HIDE_SECTION_HEADERS) {
-                    return 0;
-                } else {
-                    return mAppsPerRow;
-                }
+                return mAppsPerRow;
             } else {
                 return 1;
             }
@@ -290,10 +286,8 @@
         mTouchListener = touchListener;
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
-        if (!AppsContainerView.GRID_HIDE_SECTION_HEADERS) {
-            mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
-            mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.apps_grid_section_y_offset);
-        }
+        mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
+        mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.apps_grid_section_y_offset);
         mPaddingStart = res.getDimensionPixelSize(R.dimen.apps_container_inset);
 
         mSectionTextPaint = new Paint();
@@ -342,10 +336,7 @@
      */
     public RecyclerView.ItemDecoration getItemDecoration() {
         // We don't draw any headers when we are uncomfortably dense
-        if (!AppsContainerView.GRID_HIDE_SECTION_HEADERS) {
-            return mItemDecoration;
-        }
-        return null;
+        return mItemDecoration;
     }
 
     /**
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 7a6e9a2..3bbf0e7 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -437,13 +437,6 @@
     }
 
     public boolean updateAppsViewNumCols(Resources res, int containerWidth) {
-        if (AppsContainerView.GRID_HIDE_SECTION_HEADERS) {
-            if (appsViewNumCols != allAppsNumCols) {
-                appsViewNumCols = allAppsNumCols;
-                return true;
-            }
-            return false;
-        }
         int appsViewLeftMarginPx =
                 res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
         int availableAppsWidthPx = (containerWidth > 0) ? containerWidth : availableWidthPx;
diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
index 18cdc81..ec1fb66 100644
--- a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
+++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
@@ -52,12 +52,15 @@
  */
 public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
 
+    private static final String MID_DOT = "\u2219";
+
     private Object mAlphabeticIndex;
     private Method mAddLabelsMethod;
     private Method mSetMaxLabelCountMethod;
     private Method mGetBucketIndexMethod;
     private Method mGetBucketLabelMethod;
     private boolean mHasValidAlphabeticIndex;
+    private String mDefaultMiscLabel;
 
     public AlphabeticIndexCompat(Context context) {
         super();
@@ -72,12 +75,20 @@
             mAlphabeticIndex = ctor.newInstance(curLocale);
             try {
                 // Ensure we always have some base English locale buckets
-                if (!curLocale.getLanguage().equals(new Locale("en").getLanguage())) {
+                if (!curLocale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
                     mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH);
                 }
             } catch (Exception e) {
                 e.printStackTrace();
             }
+            if (curLocale.getLanguage().equals(Locale.JAPANESE.getLanguage())) {
+                // Japanese character 他 ("misc")
+                mDefaultMiscLabel = "\u4ed6";
+                // TODO(winsonc, omakoto): We need to handle Japanese sections better, especially the kanji
+            } else {
+                // Dot
+                mDefaultMiscLabel = MID_DOT;
+            }
             mHasValidAlphabeticIndex = true;
         } catch (Exception e) {
             mHasValidAlphabeticIndex = false;
@@ -107,13 +118,21 @@
         String s = Utilities.trim(cs);
         String sectionName = getBucketLabel(getBucketIndex(s));
         if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) {
-            boolean startsWithDigit = Character.isDigit(s.codePointAt(0));
+            int c = s.codePointAt(0);
+            boolean startsWithDigit = Character.isDigit(c);
             if (startsWithDigit) {
                 // Digit section
                 return "#";
             } else {
-                // Unknown section
-                return "\u2022";
+                boolean startsWithLetter = Character.isLetter(c);
+                if (startsWithLetter) {
+                    return mDefaultMiscLabel;
+                } else {
+                    // In languages where these differ, this ensures that we differentiate
+                    // between the misc section in the native language and a misc section
+                    // for everything else.
+                    return MID_DOT;
+                }
             }
         }
         return sectionName;