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
This commit is contained in:
Winson Chung
2015-05-11 22:12:38 -07:00
parent f6d7f4f21c
commit a3499dc019
4 changed files with 122 additions and 46 deletions
@@ -14,23 +14,28 @@ import java.util.Collections;
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 @@ class AppNameComparator {
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 @@ public class AlphabeticalAppsList {
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 @@ public class AlphabeticalAppsList {
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 @@ public class AlphabeticalAppsList {
* 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 @@ public class AlphabeticalAppsList {
*/
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 @@ public class AlphabeticalAppsList {
}
// 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 @@ public class AlphabeticalAppsList {
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);
}
+4 -13
View File
@@ -63,11 +63,7 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
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 @@ class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
*/
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;
}
/**
@@ -437,13 +437,6 @@ public class DeviceProfile {
}
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;
@@ -52,12 +52,15 @@ class BaseAlphabeticIndex {
*/
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 @@ public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
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 @@ public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
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;