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