aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java
diff options
context:
space:
mode:
authorYohei Yukawa <yukawa@google.com>2014-07-07 17:57:12 +0900
committerYohei Yukawa <yukawa@google.com>2014-07-07 20:13:55 +0900
commitece4548eb51cfeedee7d0323d451374629080019 (patch)
treef5e907fa693c69e620953a21d9ebe7b690553033 /java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java
parent513784e8086a45a7e62c736c862c4df328235617 (diff)
downloadlatinime-ece4548eb51cfeedee7d0323d451374629080019.tar.gz
latinime-ece4548eb51cfeedee7d0323d451374629080019.tar.xz
latinime-ece4548eb51cfeedee7d0323d451374629080019.zip
Ensure each character is coverted by at most one LocaleSpan
This is a groundwork to attach LocaleSpan for committed text in LatinIME. This CL adds a utility method to ensure that a given range of the text is coverted by at most one LocaleSpan. Of course it could be possible to allow a substring to be coverted by multiple LocaleSpans at the same time, but ensuring uniqueness for LocaleSpan is supposed to be a good starting point. BUG: 16029304 Change-Id: Ic33a7178d0df1f05d3626aeb5773ec902254703f
Diffstat (limited to 'java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java')
-rw-r--r--java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java175
1 files changed, 170 insertions, 5 deletions
diff --git a/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java b/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java
index 967645878..f411f181b 100644
--- a/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java
@@ -16,30 +16,37 @@
package com.android.inputmethod.compat;
+import android.text.Spannable;
+import android.text.style.LocaleSpan;
+import android.util.Log;
+
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.compat.CompatUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.Locale;
@UsedForTesting
public final class LocaleSpanCompatUtils {
+ private static final String TAG = LocaleSpanCompatUtils.class.getSimpleName();
+
// Note that LocaleSpan(Locale locale) has been introduced in API level 17
// (Build.VERSION_CODE.JELLY_BEAN_MR1).
- private static Class<?> getLocalSpanClass() {
+ private static Class<?> getLocaleSpanClass() {
try {
return Class.forName("android.text.style.LocaleSpan");
} catch (ClassNotFoundException e) {
return null;
}
}
+ private static final Class<?> LOCALE_SPAN_TYPE;
private static final Constructor<?> LOCALE_SPAN_CONSTRUCTOR;
private static final Method LOCALE_SPAN_GET_LOCALE;
static {
- final Class<?> localeSpanClass = getLocalSpanClass();
- LOCALE_SPAN_CONSTRUCTOR = CompatUtils.getConstructor(localeSpanClass, Locale.class);
- LOCALE_SPAN_GET_LOCALE = CompatUtils.getMethod(localeSpanClass, "getLocale");
+ LOCALE_SPAN_TYPE = getLocaleSpanClass();
+ LOCALE_SPAN_CONSTRUCTOR = CompatUtils.getConstructor(LOCALE_SPAN_TYPE, Locale.class);
+ LOCALE_SPAN_GET_LOCALE = CompatUtils.getMethod(LOCALE_SPAN_TYPE, "getLocale");
}
@UsedForTesting
@@ -56,4 +63,162 @@ public final class LocaleSpanCompatUtils {
public static Locale getLocaleFromLocaleSpan(final Object localeSpan) {
return (Locale) CompatUtils.invoke(localeSpan, null, LOCALE_SPAN_GET_LOCALE);
}
+
+ /**
+ * Ensures that the specified range is covered with only one {@link LocaleSpan} with the given
+ * locale. If the region is already covered by one or more {@link LocaleSpan}, their ranges are
+ * updated so that each character has only one locale.
+ * @param spannable the spannable object to be updated.
+ * @param start the start index from which {@link LocaleSpan} is attached (inclusive).
+ * @param end the end index to which {@link LocaleSpan} is attached (exclusive).
+ * @param locale the locale to be attached to the specified range.
+ */
+ @UsedForTesting
+ public static void updateLocaleSpan(final Spannable spannable, final int start,
+ final int end, final Locale locale) {
+ if (end < start) {
+ Log.e(TAG, "Invalid range: start=" + start + " end=" + end);
+ return;
+ }
+ if (!isLocaleSpanAvailable()) {
+ return;
+ }
+ // A brief summary of our strategy;
+ // 1. Enumerate all LocaleSpans between [start - 1, end + 1].
+ // 2. For each LocaleSpan S:
+ // - Update the range of S so as not to cover [start, end] if S doesn't have the
+ // expected locale.
+ // - Mark S as "to be merged" if S has the expected locale.
+ // 3. Merge all the LocaleSpans that are marked as "to be merged" into one LocaleSpan.
+ // If no appropriate span is found, create a new one with newLocaleSpan method.
+ final int searchStart = Math.max(start - 1, 0);
+ final int searchEnd = Math.min(end + 1, spannable.length());
+ // LocaleSpans found in the target range. See the step 1 in the above comment.
+ final Object[] existingLocaleSpans = spannable.getSpans(searchStart, searchEnd,
+ LOCALE_SPAN_TYPE);
+ // LocaleSpans that are marked as "to be merged". See the step 2 in the above comment.
+ final ArrayList<Object> existingLocaleSpansToBeMerged = new ArrayList<>();
+ boolean isStartExclusive = true;
+ boolean isEndExclusive = true;
+ int newStart = start;
+ int newEnd = end;
+ for (final Object existingLocaleSpan : existingLocaleSpans) {
+ final Locale attachedLocale = getLocaleFromLocaleSpan(existingLocaleSpan);
+ if (!locale.equals(attachedLocale)) {
+ // This LocaleSpan does not have the expected locale. Update its range if it has
+ // an intersection with the range [start, end] (the first case of the step 2 in the
+ // above comment).
+ removeLocaleSpanFromRange(existingLocaleSpan, spannable, start, end);
+ continue;
+ }
+ final int spanStart = spannable.getSpanStart(existingLocaleSpan);
+ final int spanEnd = spannable.getSpanEnd(existingLocaleSpan);
+ if (spanEnd < spanStart) {
+ Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd);
+ continue;
+ }
+ if (spanEnd < start || end < spanStart) {
+ // No intersection found.
+ continue;
+ }
+
+ // Here existingLocaleSpan has the expected locale and an intersection with the
+ // range [start, end] (the second case of the the step 2 in the above comment).
+ final int spanFlag = spannable.getSpanFlags(existingLocaleSpan);
+ if (spanStart < newStart) {
+ newStart = spanStart;
+ isStartExclusive = ((spanFlag & Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) ==
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (newEnd < spanEnd) {
+ newEnd = spanEnd;
+ isEndExclusive = ((spanFlag & Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) ==
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ existingLocaleSpansToBeMerged.add(existingLocaleSpan);
+ }
+
+ int originalLocaleSpanFlag = 0;
+ Object localeSpan = null;
+ if (existingLocaleSpansToBeMerged.isEmpty()) {
+ // If there is no LocaleSpan that is marked as to be merged, create a new one.
+ localeSpan = newLocaleSpan(locale);
+ } else {
+ // Reuse the first LocaleSpan to avoid unnecessary object instantiation.
+ localeSpan = existingLocaleSpansToBeMerged.get(0);
+ originalLocaleSpanFlag = spannable.getSpanFlags(localeSpan);
+ // No need to keep other instances.
+ for (int i = 1; i < existingLocaleSpansToBeMerged.size(); ++i) {
+ spannable.removeSpan(existingLocaleSpansToBeMerged.get(i));
+ }
+ }
+ final int localeSpanFlag = getSpanFlag(originalLocaleSpanFlag, isStartExclusive,
+ isEndExclusive);
+ spannable.setSpan(localeSpan, newStart, newEnd, localeSpanFlag);
+ }
+
+ private static void removeLocaleSpanFromRange(final Object localeSpan,
+ final Spannable spannable, final int removeStart, final int removeEnd) {
+ if (!isLocaleSpanAvailable()) {
+ return;
+ }
+ final int spanStart = spannable.getSpanStart(localeSpan);
+ final int spanEnd = spannable.getSpanEnd(localeSpan);
+ if (spanStart > spanEnd) {
+ Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd);
+ return;
+ }
+ if (spanEnd < removeStart) {
+ // spanStart < spanEnd < removeStart < removeEnd
+ return;
+ }
+ if (removeEnd < spanStart) {
+ // spanStart < removeEnd < spanStart < spanEnd
+ return;
+ }
+ final int spanFlags = spannable.getSpanFlags(localeSpan);
+ if (spanStart < removeStart) {
+ if (removeEnd < spanEnd) {
+ // spanStart < removeStart < removeEnd < spanEnd
+ final Locale locale = getLocaleFromLocaleSpan(localeSpan);
+ spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags);
+ final Object attionalLocaleSpan = newLocaleSpan(locale);
+ spannable.setSpan(attionalLocaleSpan, removeEnd, spanEnd, spanFlags);
+ return;
+ }
+ // spanStart < removeStart < spanEnd <= removeEnd
+ spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags);
+ return;
+ }
+ if (removeEnd < spanEnd) {
+ // removeStart <= spanStart < removeEnd < spanEnd
+ spannable.setSpan(localeSpan, removeEnd, spanEnd, spanFlags);
+ return;
+ }
+ // removeStart <= spanStart < spanEnd < removeEnd
+ spannable.removeSpan(localeSpan);
+ }
+
+ private static int getSpanFlag(final int originalFlag,
+ final boolean isStartExclusive, final boolean isEndExclusive) {
+ return (originalFlag & ~Spannable.SPAN_POINT_MARK_MASK) |
+ getSpanPointMarkFlag(isStartExclusive, isEndExclusive);
+ }
+
+ private static int getSpanPointMarkFlag(final boolean isStartExclusive,
+ final boolean isEndExclusive) {
+ if (isStartExclusive) {
+ if (isEndExclusive) {
+ return Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
+ } else {
+ return Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
+ }
+ } else {
+ if (isEndExclusive) {
+ return Spannable.SPAN_INCLUSIVE_EXCLUSIVE;
+ } else {
+ return Spannable.SPAN_INCLUSIVE_INCLUSIVE;
+ }
+ }
+ }
}