• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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).
getLocaleSpanClass()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
isLocaleSpanAvailable()53     public static boolean isLocaleSpanAvailable() {
54         return (LOCALE_SPAN_CONSTRUCTOR != null && LOCALE_SPAN_GET_LOCALE != null);
55     }
56 
57     @UsedForTesting
newLocaleSpan(final Locale locale)58     public static Object newLocaleSpan(final Locale locale) {
59         return CompatUtils.newInstance(LOCALE_SPAN_CONSTRUCTOR, locale);
60     }
61 
62     @UsedForTesting
getLocaleFromLocaleSpan(final Object localeSpan)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
updateLocaleSpan(final Spannable spannable, final int start, final int end, final Locale locale)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 
removeLocaleSpanFromRange(final Object localeSpan, final Spannable spannable, final int removeStart, final int removeEnd)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 
getSpanFlag(final int originalFlag, final boolean isStartExclusive, final boolean isEndExclusive)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 
getSpanPointMarkFlag(final boolean isStartExclusive, final boolean isEndExclusive)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