1 /*
2  * Copyright (C) 2016 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.server.inputmethod;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.icu.util.ULocale;
25 import android.os.LocaleList;
26 import android.text.TextUtils;
27 import android.util.ArrayMap;
28 
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.List;
32 import java.util.Locale;
33 
34 final class LocaleUtils {
35     public interface LocaleExtractor<T> {
36         @Nullable
get(@ullable T source)37         Locale get(@Nullable T source);
38     }
39 
40     /**
41      * Calculates a matching score for the single desired locale.
42      *
43      * @see LocaleUtils#filterByLanguage(List, LocaleExtractor, LocaleList, ArrayList)
44      *
45      * @param supported The locale supported by IME subtype.
46      * @param desired The locale preferred by user.
47      * @return A score based on the locale matching for the default subtype enabling.
48      */
49     @IntRange(from = 1, to = 4)
calculateMatchingSubScore(@onNull final ULocale supported, @NonNull final ULocale desired)50     private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
51             @NonNull final ULocale desired) {
52         // Assuming supported/desired is fully expanded.
53         if (supported.equals(desired)) {
54             return 4;  // Exact match.
55         }
56 
57         // addLikelySubtags is a maximization process as per
58         // https://www.unicode.org/reports/tr35/#Likely_Subtags
59         ULocale maxDesired = ULocale.addLikelySubtags(desired);
60 
61         // Skip language matching since it was already done in calculateMatchingScore.
62 
63         final String supportedScript = supported.getScript();
64         if (supportedScript.isEmpty() || !supportedScript.equals(maxDesired.getScript())) {
65             // TODO: Need subscript matching. For example, Hanb should match with Bopo.
66             return 1;
67         }
68 
69         final String supportedCountry = supported.getCountry();
70         if (supportedCountry.isEmpty() || !supportedCountry.equals(maxDesired.getCountry())) {
71             return 2;
72         }
73 
74         // Ignore others e.g. variants, extensions.
75 
76         // Since addLikelySubtags can canonicalize subtags, e.g. the deprecated country codes
77         // an locale with an identical script and country before addLikelySubtags is in favour,
78         // and a score of 4 is returned.
79         String desiredScript = desired.getScript();
80         String desiredCountry = desired.getCountry();
81         if ((desiredScript.isEmpty() || desiredScript.equals(maxDesired.getScript()))
82                 && (desiredCountry.isEmpty() || desiredCountry.equals(maxDesired.getCountry()))) {
83             return 4;
84         } else {
85             return 3;
86         }
87     }
88 
89     private static final class ScoreEntry implements Comparable<ScoreEntry> {
90         public int mIndex = -1;
91         @NonNull public final byte[] mScore;  // matching score of the i-th system languages.
92 
ScoreEntry(@onNull byte[] score, int index)93         ScoreEntry(@NonNull byte[] score, int index) {
94             mScore = new byte[score.length];
95             set(score, index);
96         }
97 
set(@onNull byte[] score, int index)98         private void set(@NonNull byte[] score, int index) {
99             for (int i = 0; i < mScore.length; ++i) {
100                 mScore[i] = score[i];
101             }
102             mIndex = index;
103         }
104 
105         /**
106          * Update score and index if the given score is better than this.
107          */
updateIfBetter(@onNull byte[] score, int index)108         public void updateIfBetter(@NonNull byte[] score, int index) {
109             if (compare(mScore, score) == -1) {  // mScore < score
110                 set(score, index);
111             }
112         }
113 
114         /**
115          * Provides comaprison for bytes[].
116          *
117          * <p> Comparison does as follows. If the first value of {@code left} is larger than the
118          * first value of {@code right}, {@code left} is large than {@code right}.  If the first
119          * value of {@code left} is less than the first value of {@code right}, {@code left} is less
120          * than {@code right}. If the first value of {@code left} and the first value of
121          * {@code right} is equal, do the same comparison to the next value. Finally if all values
122          * in {@code left} and {@code right} are equal, {@code left} and {@code right} is equal.</p>
123          *
124          * @param left The length must be equal to {@code right}.
125          * @param right The length must be equal to {@code left}.
126          * @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than
127          * {@code right}. 0 if {@code left} and {@code right} is equal.
128          */
129         @IntRange(from = -1, to = 1)
compare(@onNull byte[] left, @NonNull byte[] right)130         private static int compare(@NonNull byte[] left, @NonNull byte[] right) {
131             for (int i = 0; i < left.length; ++i) {
132                 if (left[i] > right[i]) {
133                     return 1;
134                 } else if (left[i] < right[i]) {
135                     return -1;
136                 }
137             }
138             return 0;
139         }
140 
141         @Override
compareTo(final ScoreEntry other)142         public int compareTo(final ScoreEntry other) {
143             return -1 * compare(mScore, other.mScore);  // Order by descending order.
144         }
145     }
146 
147     /**
148      * Filters the given items based on language preferences.
149      *
150      * <p>For each language found in {@code preferredLocales}, this method tries to copy at most
151      * one best-match item from {@code source} to {@code dest}.  For example, if
152      * {@code "en-GB", "ja", "en-AU", "fr-CA", "en-IN"} is specified to {@code preferredLocales},
153      * this method tries to copy at most one English locale, at most one Japanese, and at most one
154      * French locale from {@code source} to {@code dest}.  Here the best matching English locale
155      * will be searched from {@code source} based on matching score. For the score design, see
156      * {@link LocaleUtils#calculateMatchingSubScore(ULocale, ULocale)}</p>
157      *
158      * @param sources Source items to be filtered.
159      * @param extractor Type converter from the source items to {@link Locale} object.
160      * @param preferredLocales Ordered list of locales with which the input items will be
161      * filtered.
162      * @param dest Destination into which the filtered items will be added.
163      * @param <T> Type of the data items.
164      */
filterByLanguage( @onNull List<T> sources, @NonNull LocaleExtractor<T> extractor, @NonNull LocaleList preferredLocales, @NonNull ArrayList<T> dest)165     public static <T> void filterByLanguage(
166             @NonNull List<T> sources,
167             @NonNull LocaleExtractor<T> extractor,
168             @NonNull LocaleList preferredLocales,
169             @NonNull ArrayList<T> dest) {
170         if (preferredLocales.isEmpty()) {
171             return;
172         }
173 
174         final int numPreferredLocales = preferredLocales.size();
175         final ArrayMap<String, ScoreEntry> scoreboard = new ArrayMap<>();
176         final byte[] score = new byte[numPreferredLocales];
177         final ULocale[] preferredULocaleCache = new ULocale[numPreferredLocales];
178 
179         final int sourceSize = sources.size();
180         for (int i = 0; i < sourceSize; ++i) {
181             final Locale locale = extractor.get(sources.get(i));
182             if (locale == null) {
183                 continue;
184             }
185 
186             boolean canSkip = true;
187             for (int j = 0; j < numPreferredLocales; ++j) {
188                 final Locale preferredLocale = preferredLocales.get(j);
189                 if (!TextUtils.equals(locale.getLanguage(), preferredLocale.getLanguage())) {
190                     score[j] = 0;
191                     continue;
192                 }
193                 if (preferredULocaleCache[j] == null) {
194                     preferredULocaleCache[j] = ULocale.addLikelySubtags(
195                             ULocale.forLocale(preferredLocale));
196                 }
197                 score[j] = calculateMatchingSubScore(
198                         preferredULocaleCache[j], ULocale.forLocale(locale));
199                 if (canSkip && score[j] != 0) {
200                     canSkip = false;
201                 }
202             }
203             if (canSkip) {
204                 continue;
205             }
206 
207             final String lang = locale.getLanguage();
208             final ScoreEntry bestScore = scoreboard.get(lang);
209             if (bestScore == null) {
210                 scoreboard.put(lang, new ScoreEntry(score, i));
211             } else {
212                 bestScore.updateIfBetter(score, i);
213             }
214         }
215 
216         final int numEntries = scoreboard.size();
217         final ScoreEntry[] result = new ScoreEntry[numEntries];
218         for (int i = 0; i < numEntries; ++i) {
219             result[i] = scoreboard.valueAt(i);
220         }
221         Arrays.sort(result);
222         for (final ScoreEntry entry : result) {
223             dest.add(sources.get(entry.mIndex));
224         }
225     }
226 
227     /**
228      * Returns the language component of a given locale string.
229      * TODO(b/321064051): Switch to {@link
230      * com.android.internal.inputmethod.SubtypeLocaleUtils#constructLocaleFromString(String)}
231      */
getLanguageFromLocaleString(String locale)232     static String getLanguageFromLocaleString(String locale) {
233         final int idx = locale.indexOf('_');
234         if (idx < 0) {
235             return locale;
236         } else {
237             return locale.substring(0, idx);
238         }
239     }
240 
getSystemLocaleFromContext(Context context)241     static Locale getSystemLocaleFromContext(Context context) {
242         try {
243             return context.getResources().getConfiguration().locale;
244         } catch (Resources.NotFoundException ex) {
245             return null;
246         }
247     }
248 }
249