1 package com.android.launcher3.compat; 2 3 import android.annotation.TargetApi; 4 import android.content.Context; 5 import android.icu.text.AlphabeticIndex; 6 import android.os.Build; 7 import android.os.LocaleList; 8 import android.util.Log; 9 10 import com.android.launcher3.Utilities; 11 12 import java.lang.reflect.Method; 13 import java.util.Locale; 14 15 public class AlphabeticIndexCompat { 16 private static final String TAG = "AlphabeticIndexCompat"; 17 18 private static final String MID_DOT = "\u2219"; 19 private final BaseIndex mBaseIndex; 20 private final String mDefaultMiscLabel; 21 22 public AlphabeticIndexCompat(Context context) { 23 BaseIndex index = null; 24 25 try { 26 if (Utilities.isNycOrAbove()) { 27 index = new AlphabeticIndexVN(context); 28 } 29 } catch (Exception e) { 30 Log.d(TAG, "Unable to load the system index", e); 31 } 32 if (index == null) { 33 try { 34 index = new AlphabeticIndexV16(context); 35 } catch (Exception e) { 36 Log.d(TAG, "Unable to load the system index", e); 37 } 38 } 39 40 mBaseIndex = index == null ? new BaseIndex() : index; 41 42 if (context.getResources().getConfiguration().locale 43 .getLanguage().equals(Locale.JAPANESE.getLanguage())) { 44 // Japanese character ("misc") 45 mDefaultMiscLabel = "\u4ed6"; 46 // TODO(winsonc, omakoto): We need to handle Japanese sections better, especially the kanji 47 } else { 48 // Dot 49 mDefaultMiscLabel = MID_DOT; 50 } 51 } 52 53 /** 54 * Computes the section name for an given string {@param s}. 55 */ 56 public String computeSectionName(CharSequence cs) { 57 String s = Utilities.trim(cs); 58 String sectionName = mBaseIndex.getBucketLabel(mBaseIndex.getBucketIndex(s)); 59 if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) { 60 int c = s.codePointAt(0); 61 boolean startsWithDigit = Character.isDigit(c); 62 if (startsWithDigit) { 63 // Digit section 64 return "#"; 65 } else { 66 boolean startsWithLetter = Character.isLetter(c); 67 if (startsWithLetter) { 68 return mDefaultMiscLabel; 69 } else { 70 // In languages where these differ, this ensures that we differentiate 71 // between the misc section in the native language and a misc section 72 // for everything else. 73 return MID_DOT; 74 } 75 } 76 } 77 return sectionName; 78 } 79 80 /** 81 * Base class to support Alphabetic indexing if not supported by the framework. 82 * TODO(winsonc): disable for non-english locales 83 */ 84 private static class BaseIndex { 85 86 private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-"; 87 private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1; 88 89 /** 90 * Returns the index of the bucket in which the given string should appear. 91 */ 92 protected int getBucketIndex(String s) { 93 if (s.isEmpty()) { 94 return UNKNOWN_BUCKET_INDEX; 95 } 96 int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase()); 97 if (index != -1) { 98 return index; 99 } 100 return UNKNOWN_BUCKET_INDEX; 101 } 102 103 /** 104 * Returns the label for the bucket at the given index (as returned by getBucketIndex). 105 */ 106 protected String getBucketLabel(int index) { 107 return BUCKETS.substring(index, index + 1); 108 } 109 } 110 111 /** 112 * Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base 113 * alphabetic index. 114 */ 115 private static class AlphabeticIndexV16 extends BaseIndex { 116 117 private Object mAlphabeticIndex; 118 private Method mGetBucketIndexMethod; 119 private Method mGetBucketLabelMethod; 120 121 public AlphabeticIndexV16(Context context) throws Exception { 122 Locale curLocale = context.getResources().getConfiguration().locale; 123 Class clazz = Class.forName("libcore.icu.AlphabeticIndex"); 124 mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class); 125 mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class); 126 mAlphabeticIndex = clazz.getConstructor(Locale.class).newInstance(curLocale); 127 128 if (!curLocale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { 129 clazz.getDeclaredMethod("addLabels", Locale.class) 130 .invoke(mAlphabeticIndex, Locale.ENGLISH); 131 } 132 } 133 134 /** 135 * Returns the index of the bucket in which {@param s} should appear. 136 * Function is synchronized because underlying routine walks an iterator 137 * whose state is maintained inside the index object. 138 */ 139 protected int getBucketIndex(String s) { 140 try { 141 return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s); 142 } catch (Exception e) { 143 e.printStackTrace(); 144 } 145 return super.getBucketIndex(s); 146 } 147 148 /** 149 * Returns the label for the bucket at the given index (as returned by getBucketIndex). 150 */ 151 protected String getBucketLabel(int index) { 152 try { 153 return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index); 154 } catch (Exception e) { 155 e.printStackTrace(); 156 } 157 return super.getBucketLabel(index); 158 } 159 } 160 161 /** 162 * Implementation based on {@link AlphabeticIndex}. 163 */ 164 @TargetApi(Build.VERSION_CODES.N) 165 private static class AlphabeticIndexVN extends BaseIndex { 166 167 private final AlphabeticIndex.ImmutableIndex mAlphabeticIndex; 168 169 public AlphabeticIndexVN(Context context) { 170 LocaleList locales = context.getResources().getConfiguration().getLocales(); 171 int localeCount = locales.size(); 172 173 Locale primaryLocale = localeCount == 0 ? Locale.ENGLISH : locales.get(0); 174 AlphabeticIndex indexBuilder = new AlphabeticIndex(primaryLocale); 175 for (int i = 1; i < localeCount; i++) { 176 indexBuilder.addLabels(locales.get(i)); 177 } 178 indexBuilder.addLabels(Locale.ENGLISH); 179 180 mAlphabeticIndex = indexBuilder.buildImmutableIndex(); 181 } 182 183 /** 184 * Returns the index of the bucket in which {@param s} should appear. 185 */ 186 protected int getBucketIndex(String s) { 187 return mAlphabeticIndex.getBucketIndex(s); 188 } 189 190 /** 191 * Returns the label for the bucket at the given index 192 */ 193 protected String getBucketLabel(int index) { 194 return mAlphabeticIndex.getBucket(index).getLabel(); 195 } 196 } 197 } 198