Home | History | Annotate | Download | only in compat
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.compat;
     18 
     19 import android.text.Spannable;
     20 import android.text.style.LocaleSpan;
     21 import android.util.Log;
     22 
     23 import com.android.inputmethod.annotations.UsedForTesting;
     24 
     25 import java.lang.reflect.Constructor;
     26 import java.lang.reflect.Method;
     27 import java.util.ArrayList;
     28 import java.util.Locale;
     29 
     30 @UsedForTesting
     31 public final class LocaleSpanCompatUtils {
     32     private static final String TAG = LocaleSpanCompatUtils.class.getSimpleName();
     33 
     34     // Note that LocaleSpan(Locale locale) has been introduced in API level 17
     35     // (Build.VERSION_CODE.JELLY_BEAN_MR1).
     36     private static Class<?> getLocaleSpanClass() {
     37         try {
     38             return Class.forName("android.text.style.LocaleSpan");
     39         } catch (ClassNotFoundException e) {
     40             return null;
     41         }
     42     }
     43     private static final Class<?> LOCALE_SPAN_TYPE;
     44     private static final Constructor<?> LOCALE_SPAN_CONSTRUCTOR;
     45     private static final Method LOCALE_SPAN_GET_LOCALE;
     46     static {
     47         LOCALE_SPAN_TYPE = getLocaleSpanClass();
     48         LOCALE_SPAN_CONSTRUCTOR = CompatUtils.getConstructor(LOCALE_SPAN_TYPE, Locale.class);
     49         LOCALE_SPAN_GET_LOCALE = CompatUtils.getMethod(LOCALE_SPAN_TYPE, "getLocale");
     50     }
     51 
     52     @UsedForTesting
     53     public static boolean isLocaleSpanAvailable() {
     54         return (LOCALE_SPAN_CONSTRUCTOR != null && LOCALE_SPAN_GET_LOCALE != null);
     55     }
     56 
     57     @UsedForTesting
     58     public static Object newLocaleSpan(final Locale locale) {
     59         return CompatUtils.newInstance(LOCALE_SPAN_CONSTRUCTOR, locale);
     60     }
     61 
     62     @UsedForTesting
     63     public static Locale getLocaleFromLocaleSpan(final Object localeSpan) {
     64         return (Locale) CompatUtils.invoke(localeSpan, null, LOCALE_SPAN_GET_LOCALE);
     65     }
     66 
     67     /**
     68      * Ensures that the specified range is covered with only one {@link LocaleSpan} with the given
     69      * locale. If the region is already covered by one or more {@link LocaleSpan}, their ranges are
     70      * updated so that each character has only one locale.
     71      * @param spannable the spannable object to be updated.
     72      * @param start the start index from which {@link LocaleSpan} is attached (inclusive).
     73      * @param end the end index to which {@link LocaleSpan} is attached (exclusive).
     74      * @param locale the locale to be attached to the specified range.
     75      */
     76     @UsedForTesting
     77     public static void updateLocaleSpan(final Spannable spannable, final int start,
     78             final int end, final Locale locale) {
     79         if (end < start) {
     80             Log.e(TAG, "Invalid range: start=" + start + " end=" + end);
     81             return;
     82         }
     83         if (!isLocaleSpanAvailable()) {
     84             return;
     85         }
     86         // A brief summary of our strategy;
     87         //   1. Enumerate all LocaleSpans between [start - 1, end + 1].
     88         //   2. For each LocaleSpan S:
     89         //      - Update the range of S so as not to cover [start, end] if S doesn't have the
     90         //        expected locale.
     91         //      - Mark S as "to be merged" if S has the expected locale.
     92         //   3. Merge all the LocaleSpans that are marked as "to be merged" into one LocaleSpan.
     93         //      If no appropriate span is found, create a new one with newLocaleSpan method.
     94         final int searchStart = Math.max(start - 1, 0);
     95         final int searchEnd = Math.min(end + 1, spannable.length());
     96         // LocaleSpans found in the target range. See the step 1 in the above comment.
     97         final Object[] existingLocaleSpans = spannable.getSpans(searchStart, searchEnd,
     98                 LOCALE_SPAN_TYPE);
     99         // LocaleSpans that are marked as "to be merged". See the step 2 in the above comment.
    100         final ArrayList<Object> existingLocaleSpansToBeMerged = new ArrayList<>();
    101         boolean isStartExclusive = true;
    102         boolean isEndExclusive = true;
    103         int newStart = start;
    104         int newEnd = end;
    105         for (final Object existingLocaleSpan : existingLocaleSpans) {
    106             final Locale attachedLocale = getLocaleFromLocaleSpan(existingLocaleSpan);
    107             if (!locale.equals(attachedLocale)) {
    108                 // This LocaleSpan does not have the expected locale. Update its range if it has
    109                 // an intersection with the range [start, end] (the first case of the step 2 in the
    110                 // above comment).
    111                 removeLocaleSpanFromRange(existingLocaleSpan, spannable, start, end);
    112                 continue;
    113             }
    114             final int spanStart = spannable.getSpanStart(existingLocaleSpan);
    115             final int spanEnd = spannable.getSpanEnd(existingLocaleSpan);
    116             if (spanEnd < spanStart) {
    117                 Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd);
    118                 continue;
    119             }
    120             if (spanEnd < start || end < spanStart) {
    121                 // No intersection found.
    122                 continue;
    123             }
    124 
    125             // Here existingLocaleSpan has the expected locale and an intersection with the
    126             // range [start, end] (the second case of the the step 2 in the above comment).
    127             final int spanFlag = spannable.getSpanFlags(existingLocaleSpan);
    128             if (spanStart < newStart) {
    129                 newStart = spanStart;
    130                 isStartExclusive = ((spanFlag & Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) ==
    131                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    132             }
    133             if (newEnd < spanEnd) {
    134                 newEnd = spanEnd;
    135                 isEndExclusive = ((spanFlag & Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) ==
    136                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    137             }
    138             existingLocaleSpansToBeMerged.add(existingLocaleSpan);
    139         }
    140 
    141         int originalLocaleSpanFlag = 0;
    142         Object localeSpan = null;
    143         if (existingLocaleSpansToBeMerged.isEmpty()) {
    144             // If there is no LocaleSpan that is marked as to be merged, create a new one.
    145             localeSpan = newLocaleSpan(locale);
    146         } else {
    147             // Reuse the first LocaleSpan to avoid unnecessary object instantiation.
    148             localeSpan = existingLocaleSpansToBeMerged.get(0);
    149             originalLocaleSpanFlag = spannable.getSpanFlags(localeSpan);
    150             // No need to keep other instances.
    151             for (int i = 1; i < existingLocaleSpansToBeMerged.size(); ++i) {
    152                 spannable.removeSpan(existingLocaleSpansToBeMerged.get(i));
    153             }
    154         }
    155         final int localeSpanFlag = getSpanFlag(originalLocaleSpanFlag, isStartExclusive,
    156                 isEndExclusive);
    157         spannable.setSpan(localeSpan, newStart, newEnd, localeSpanFlag);
    158     }
    159 
    160     private static void removeLocaleSpanFromRange(final Object localeSpan,
    161             final Spannable spannable, final int removeStart, final int removeEnd) {
    162         if (!isLocaleSpanAvailable()) {
    163             return;
    164         }
    165         final int spanStart = spannable.getSpanStart(localeSpan);
    166         final int spanEnd = spannable.getSpanEnd(localeSpan);
    167         if (spanStart > spanEnd) {
    168             Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd);
    169             return;
    170         }
    171         if (spanEnd < removeStart) {
    172             // spanStart < spanEnd < removeStart < removeEnd
    173             return;
    174         }
    175         if (removeEnd < spanStart) {
    176             // spanStart < removeEnd < spanStart < spanEnd
    177             return;
    178         }
    179         final int spanFlags = spannable.getSpanFlags(localeSpan);
    180         if (spanStart < removeStart) {
    181             if (removeEnd < spanEnd) {
    182                 // spanStart < removeStart < removeEnd < spanEnd
    183                 final Locale locale = getLocaleFromLocaleSpan(localeSpan);
    184                 spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags);
    185                 final Object attionalLocaleSpan = newLocaleSpan(locale);
    186                 spannable.setSpan(attionalLocaleSpan, removeEnd, spanEnd, spanFlags);
    187                 return;
    188             }
    189             // spanStart < removeStart < spanEnd <= removeEnd
    190             spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags);
    191             return;
    192         }
    193         if (removeEnd < spanEnd) {
    194             // removeStart <= spanStart < removeEnd < spanEnd
    195             spannable.setSpan(localeSpan, removeEnd, spanEnd, spanFlags);
    196             return;
    197         }
    198         // removeStart <= spanStart < spanEnd < removeEnd
    199         spannable.removeSpan(localeSpan);
    200     }
    201 
    202     private static int getSpanFlag(final int originalFlag,
    203             final boolean isStartExclusive, final boolean isEndExclusive) {
    204         return (originalFlag & ~Spannable.SPAN_POINT_MARK_MASK) |
    205                 getSpanPointMarkFlag(isStartExclusive, isEndExclusive);
    206     }
    207 
    208     private static int getSpanPointMarkFlag(final boolean isStartExclusive,
    209             final boolean isEndExclusive) {
    210         if (isStartExclusive) {
    211             if (isEndExclusive) {
    212                 return Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
    213             } else {
    214                 return Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
    215             }
    216         } else {
    217             if (isEndExclusive) {
    218                 return Spannable.SPAN_INCLUSIVE_EXCLUSIVE;
    219             } else {
    220                 return Spannable.SPAN_INCLUSIVE_INCLUSIVE;
    221             }
    222         }
    223     }
    224 }
    225