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