• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
AlphabeticIndexCompat(Context context)22     public AlphabeticIndexCompat(Context context) {
23         BaseIndex index = null;
24 
25         try {
26             if (Utilities.ATLEAST_NOUGAT) {
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      */
computeSectionName(CharSequence cs)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          */
getBucketIndex(String s)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          */
getBucketLabel(int index)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 
AlphabeticIndexV16(Context context)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          */
getBucketIndex(String s)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          */
getBucketLabel(int index)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 
AlphabeticIndexVN(Context context)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          */
getBucketIndex(String s)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          */
getBucketLabel(int index)193         protected String getBucketLabel(int index) {
194             return mAlphabeticIndex.getBucket(index).getLabel();
195         }
196     }
197 }
198