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