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 = 3)
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 3;  // Exact match.
55         }
56 
57         // Skip language matching since it was already done in calculateMatchingScore.
58 
59         final String supportedScript = supported.getScript();
60         if (supportedScript.isEmpty() || !supportedScript.equals(desired.getScript())) {
61             // TODO: Need subscript matching. For example, Hanb should match with Bopo.
62             return 1;
63         }
64 
65         final String supportedCountry = supported.getCountry();
66         if (supportedCountry.isEmpty() || !supportedCountry.equals(desired.getCountry())) {
67             return 2;
68         }
69 
70         // Ignore others e.g. variants, extensions.
71         return 3;
72     }
73 
74     private static final class ScoreEntry implements Comparable<ScoreEntry> {
75         public int mIndex = -1;
76         @NonNull public final byte[] mScore;  // matching score of the i-th system languages.
77 
ScoreEntry(@onNull byte[] score, int index)78         ScoreEntry(@NonNull byte[] score, int index) {
79             mScore = new byte[score.length];
80             set(score, index);
81         }
82 
set(@onNull byte[] score, int index)83         private void set(@NonNull byte[] score, int index) {
84             for (int i = 0; i < mScore.length; ++i) {
85                 mScore[i] = score[i];
86             }
87             mIndex = index;
88         }
89 
90         /**
91          * Update score and index if the given score is better than this.
92          */
updateIfBetter(@onNull byte[] score, int index)93         public void updateIfBetter(@NonNull byte[] score, int index) {
94             if (compare(mScore, score) == -1) {  // mScore < score
95                 set(score, index);
96             }
97         }
98 
99         /**
100          * Provides comaprison for bytes[].
101          *
102          * <p> Comparison does as follows. If the first value of {@code left} is larger than the
103          * first value of {@code right}, {@code left} is large than {@code right}.  If the first
104          * value of {@code left} is less than the first value of {@code right}, {@code left} is less
105          * than {@code right}. If the first value of {@code left} and the first value of
106          * {@code right} is equal, do the same comparison to the next value. Finally if all values
107          * in {@code left} and {@code right} are equal, {@code left} and {@code right} is equal.</p>
108          *
109          * @param left The length must be equal to {@code right}.
110          * @param right The length must be equal to {@code left}.
111          * @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than
112          * {@code right}. 0 if {@code left} and {@code right} is equal.
113          */
114         @IntRange(from = -1, to = 1)
compare(@onNull byte[] left, @NonNull byte[] right)115         private static int compare(@NonNull byte[] left, @NonNull byte[] right) {
116             for (int i = 0; i < left.length; ++i) {
117                 if (left[i] > right[i]) {
118                     return 1;
119                 } else if (left[i] < right[i]) {
120                     return -1;
121                 }
122             }
123             return 0;
124         }
125 
126         @Override
compareTo(final ScoreEntry other)127         public int compareTo(final ScoreEntry other) {
128             return -1 * compare(mScore, other.mScore);  // Order by descending order.
129         }
130     }
131 
132     /**
133      * Filters the given items based on language preferences.
134      *
135      * <p>For each language found in {@code preferredLocales}, this method tries to copy at most
136      * one best-match item from {@code source} to {@code dest}.  For example, if
137      * {@code "en-GB", "ja", "en-AU", "fr-CA", "en-IN"} is specified to {@code preferredLocales},
138      * this method tries to copy at most one English locale, at most one Japanese, and at most one
139      * French locale from {@code source} to {@code dest}.  Here the best matching English locale
140      * will be searched from {@code source} based on matching score. For the score design, see
141      * {@link LocaleUtils#calculateMatchingSubScore(ULocale, ULocale)}</p>
142      *
143      * @param sources Source items to be filtered.
144      * @param extractor Type converter from the source items to {@link Locale} object.
145      * @param preferredLocales Ordered list of locales with which the input items will be
146      * filtered.
147      * @param dest Destination into which the filtered items will be added.
148      * @param <T> Type of the data items.
149      */
filterByLanguage( @onNull List<T> sources, @NonNull LocaleExtractor<T> extractor, @NonNull LocaleList preferredLocales, @NonNull ArrayList<T> dest)150     public static <T> void filterByLanguage(
151             @NonNull List<T> sources,
152             @NonNull LocaleExtractor<T> extractor,
153             @NonNull LocaleList preferredLocales,
154             @NonNull ArrayList<T> dest) {
155         if (preferredLocales.isEmpty()) {
156             return;
157         }
158 
159         final int numPreferredLocales = preferredLocales.size();
160         final ArrayMap<String, ScoreEntry> scoreboard = new ArrayMap<>();
161         final byte[] score = new byte[numPreferredLocales];
162         final ULocale[] preferredULocaleCache = new ULocale[numPreferredLocales];
163 
164         final int sourceSize = sources.size();
165         for (int i = 0; i < sourceSize; ++i) {
166             final Locale locale = extractor.get(sources.get(i));
167             if (locale == null) {
168                 continue;
169             }
170 
171             boolean canSkip = true;
172             for (int j = 0; j < numPreferredLocales; ++j) {
173                 final Locale preferredLocale = preferredLocales.get(j);
174                 if (!TextUtils.equals(locale.getLanguage(), preferredLocale.getLanguage())) {
175                     score[j] = 0;
176                     continue;
177                 }
178                 if (preferredULocaleCache[j] == null) {
179                     preferredULocaleCache[j] = ULocale.addLikelySubtags(
180                             ULocale.forLocale(preferredLocale));
181                 }
182                 score[j] = calculateMatchingSubScore(
183                         preferredULocaleCache[j],
184                         ULocale.addLikelySubtags(ULocale.forLocale(locale)));
185                 if (canSkip && score[j] != 0) {
186                     canSkip = false;
187                 }
188             }
189             if (canSkip) {
190                 continue;
191             }
192 
193             final String lang = locale.getLanguage();
194             final ScoreEntry bestScore = scoreboard.get(lang);
195             if (bestScore == null) {
196                 scoreboard.put(lang, new ScoreEntry(score, i));
197             } else {
198                 bestScore.updateIfBetter(score, i);
199             }
200         }
201 
202         final int numEntries = scoreboard.size();
203         final ScoreEntry[] result = new ScoreEntry[numEntries];
204         for (int i = 0; i < numEntries; ++i) {
205             result[i] = scoreboard.valueAt(i);
206         }
207         Arrays.sort(result);
208         for (final ScoreEntry entry : result) {
209             dest.add(sources.get(entry.mIndex));
210         }
211     }
212 
213     /**
214      * Returns the language component of a given locale string.
215      * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
216      */
getLanguageFromLocaleString(String locale)217     static String getLanguageFromLocaleString(String locale) {
218         final int idx = locale.indexOf('_');
219         if (idx < 0) {
220             return locale;
221         } else {
222             return locale.substring(0, idx);
223         }
224     }
225 
getSystemLocaleFromContext(Context context)226     static Locale getSystemLocaleFromContext(Context context) {
227         try {
228             return context.getResources().getConfiguration().locale;
229         } catch (Resources.NotFoundException ex) {
230             return null;
231         }
232     }
233 }
234