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
17package com.android.inputmethod.compat;
18
19import android.text.Spannable;
20import android.text.Spanned;
21import android.text.style.LocaleSpan;
22import android.util.Log;
23
24import com.android.inputmethod.annotations.UsedForTesting;
25
26import java.lang.reflect.Constructor;
27import java.lang.reflect.Method;
28import java.util.ArrayList;
29import java.util.Locale;
30
31@UsedForTesting
32public 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).
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
54    public static boolean isLocaleSpanAvailable() {
55        return (LOCALE_SPAN_CONSTRUCTOR != null && LOCALE_SPAN_GET_LOCALE != null);
56    }
57
58    @UsedForTesting
59    public static Object newLocaleSpan(final Locale locale) {
60        return CompatUtils.newInstance(LOCALE_SPAN_CONSTRUCTOR, locale);
61    }
62
63    @UsedForTesting
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
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
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
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
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