Home | History | Annotate | Download | only in compat
      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