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