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