am da19daf5: Merge "Ensure each character is coverted by at most one LocaleSpan"
* commit 'da19daf570108149ffcf4867bd7dde79a119204a': Ensure each character is coverted by at most one LocaleSpan
commit
13c0789c52
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,14 @@
|
|||
|
||||
package com.android.inputmethod.compat;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -35,4 +40,173 @@ public class LocaleSpanCompatUtilsTests extends AndroidTestCase {
|
|||
assertEquals(Locale.JAPANESE,
|
||||
LocaleSpanCompatUtils.getLocaleFromLocaleSpan(japaneseLocaleSpan));
|
||||
}
|
||||
|
||||
private static void assertLocaleSpan(final Spanned spanned, final int index,
|
||||
final int expectedStart, final int expectedEnd,
|
||||
final Locale expectedLocale, final int expectedSpanFlags) {
|
||||
final Object span = spanned.getSpans(0, spanned.length(), Object.class)[index];
|
||||
assertEquals(expectedLocale, LocaleSpanCompatUtils.getLocaleFromLocaleSpan(span));
|
||||
assertEquals(expectedStart, spanned.getSpanStart(span));
|
||||
assertEquals(expectedEnd, spanned.getSpanEnd(span));
|
||||
assertEquals(expectedSpanFlags, spanned.getSpanFlags(span));
|
||||
}
|
||||
|
||||
private static void assertSpanEquals(final Object expectedSpan, final Spanned spanned,
|
||||
final int index) {
|
||||
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
|
||||
assertEquals(expectedSpan, spans[index]);
|
||||
}
|
||||
|
||||
private static void assertSpanCount(final int expectedCount, final Spanned spanned) {
|
||||
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
|
||||
assertEquals(expectedCount, spans.length);
|
||||
}
|
||||
|
||||
public void testUpdateLocaleSpan() {
|
||||
if (!LocaleSpanCompatUtils.isLocaleSpanAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Test if the simplest case works.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 1, 5, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 5, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if only LocaleSpans are updated.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
|
||||
text.setSpan(styleSpan, 0, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 1, 5, Locale.JAPANESE);
|
||||
assertSpanCount(2, text);
|
||||
assertSpanEquals(styleSpan, text, 0);
|
||||
assertLocaleSpan(text, 1, 1, 5, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if two jointed spans are merged into one span.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 1, 3,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 3, 5, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 5, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if two overlapped spans are merged into one span.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 1, 4,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 3, 5, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 5, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if three overlapped spans are merged into one span.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 1, 4,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 5, 6,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 2, 8, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 8, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if disjoint spans remain disjoint.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 1, 3,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 5, 6,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 8, 9, Locale.JAPANESE);
|
||||
assertSpanCount(3, text);
|
||||
assertLocaleSpan(text, 0, 1, 3, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
assertLocaleSpan(text, 1, 5, 6, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
assertLocaleSpan(text, 2, 8, 9, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if existing span flags are preserved during merge.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 1, 5,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE | Spannable.SPAN_INTERMEDIATE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 3, 4, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 5, Locale.JAPANESE,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE | Spannable.SPAN_INTERMEDIATE);
|
||||
}
|
||||
|
||||
// Test if existing span flags are preserved even when partially overlapped (leading edge).
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 1, 5,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE | Spannable.SPAN_INTERMEDIATE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 3, 7, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 7, Locale.JAPANESE,
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE | Spannable.SPAN_INTERMEDIATE);
|
||||
}
|
||||
|
||||
// Test if existing span flags are preserved even when partially overlapped (trailing edge).
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE), 3, 7,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE | Spannable.SPAN_INTERMEDIATE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 1, 5, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 7, Locale.JAPANESE,
|
||||
Spannable.SPAN_EXCLUSIVE_INCLUSIVE | Spannable.SPAN_INTERMEDIATE);
|
||||
}
|
||||
|
||||
// Test if existing locale span will be removed when the locale doesn't match.
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.ENGLISH), 3, 5,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 1, 7, Locale.JAPANESE);
|
||||
assertSpanCount(1, text);
|
||||
assertLocaleSpan(text, 0, 1, 7, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if existing locale span will be removed when the locale doesn't match. (case 2)
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.ENGLISH), 3, 7,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 5, 6, Locale.JAPANESE);
|
||||
assertSpanCount(3, text);
|
||||
assertLocaleSpan(text, 0, 3, 5, Locale.ENGLISH, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
assertLocaleSpan(text, 1, 6, 7, Locale.ENGLISH, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
assertLocaleSpan(text, 2, 5, 6, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if existing locale span will be removed when the locale doesn't match. (case 3)
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.ENGLISH), 3, 7,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 2, 5, Locale.JAPANESE);
|
||||
assertSpanCount(2, text);
|
||||
assertLocaleSpan(text, 0, 5, 7, Locale.ENGLISH, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
assertLocaleSpan(text, 1, 2, 5, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Test if existing locale span will be removed when the locale doesn't match. (case 3)
|
||||
{
|
||||
final SpannableString text = new SpannableString("0123456789");
|
||||
text.setSpan(LocaleSpanCompatUtils.newLocaleSpan(Locale.ENGLISH), 3, 7,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
LocaleSpanCompatUtils.updateLocaleSpan(text, 5, 8, Locale.JAPANESE);
|
||||
assertSpanCount(2, text);
|
||||
assertLocaleSpan(text, 0, 3, 5, Locale.ENGLISH, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
assertLocaleSpan(text, 1, 5, 8, Locale.JAPANESE, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue