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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user