blob: 7a6853a25e5b1974739ce2cdd95adffccde96590 [file] [log] [blame]
Yohei Yukawa102ff072016-02-24 18:25:16 -08001/*
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
Yohei Yukawae6b6e0e2018-09-12 16:42:48 -070017package com.android.server.inputmethod;
Yohei Yukawa24d8f6d2018-09-12 02:51:26 +000018
Seigo Nonaka072a95a2016-03-04 18:38:26 -080019import android.annotation.IntRange;
Yohei Yukawa102ff072016-02-24 18:25:16 -080020import android.annotation.NonNull;
21import android.annotation.Nullable;
Seigo Nonaka072a95a2016-03-04 18:38:26 -080022import android.icu.util.ULocale;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070023import android.os.LocaleList;
Yohei Yukawadce7df32017-05-04 16:28:18 -070024import android.text.TextUtils;
Yohei Yukawa7a712372018-11-20 18:46:05 -080025import android.util.ArrayMap;
Yohei Yukawa102ff072016-02-24 18:25:16 -080026
27import java.util.ArrayList;
Seigo Nonaka072a95a2016-03-04 18:38:26 -080028import java.util.Arrays;
Yohei Yukawa102ff072016-02-24 18:25:16 -080029import java.util.List;
30import java.util.Locale;
Yohei Yukawa102ff072016-02-24 18:25:16 -080031
Yohei Yukawae6b6e0e2018-09-12 16:42:48 -070032final class LocaleUtils {
Yohei Yukawa102ff072016-02-24 18:25:16 -080033 public interface LocaleExtractor<T> {
34 @Nullable
35 Locale get(@Nullable T source);
36 }
37
Seigo Nonaka072a95a2016-03-04 18:38:26 -080038 /**
39 * Calculates a matching score for the single desired locale.
40 *
Yohei Yukawadce7df32017-05-04 16:28:18 -070041 * @see LocaleUtils#filterByLanguage(List, LocaleExtractor, LocaleList, ArrayList)
Seigo Nonaka072a95a2016-03-04 18:38:26 -080042 *
43 * @param supported The locale supported by IME subtype.
44 * @param desired The locale preferred by user.
45 * @return A score based on the locale matching for the default subtype enabling.
46 */
47 @IntRange(from=1, to=3)
48 private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
49 @NonNull final ULocale desired) {
50 // Assuming supported/desired is fully expanded.
51 if (supported.equals(desired)) {
52 return 3; // Exact match.
Yohei Yukawa102ff072016-02-24 18:25:16 -080053 }
Seigo Nonaka072a95a2016-03-04 18:38:26 -080054
55 // Skip language matching since it was already done in calculateMatchingScore.
56
57 final String supportedScript = supported.getScript();
58 if (supportedScript.isEmpty() || !supportedScript.equals(desired.getScript())) {
59 // TODO: Need subscript matching. For example, Hanb should match with Bopo.
60 return 1;
61 }
62
63 final String supportedCountry = supported.getCountry();
64 if (supportedCountry.isEmpty() || !supportedCountry.equals(desired.getCountry())) {
65 return 2;
66 }
67
68 // Ignore others e.g. variants, extensions.
69 return 3;
70 }
71
Seigo Nonaka072a95a2016-03-04 18:38:26 -080072 private static final class ScoreEntry implements Comparable<ScoreEntry> {
73 public int mIndex = -1;
74 @NonNull public final byte[] mScore; // matching score of the i-th system languages.
75
76 ScoreEntry(@NonNull byte[] score, int index) {
77 mScore = new byte[score.length];
78 set(score, index);
79 }
80
81 private void set(@NonNull byte[] score, int index) {
82 for (int i = 0; i < mScore.length; ++i) {
83 mScore[i] = score[i];
84 }
85 mIndex = index;
86 }
87
88 /**
89 * Update score and index if the given score is better than this.
90 */
91 public void updateIfBetter(@NonNull byte[] score, int index) {
92 if (compare(mScore, score) == -1) { // mScore < score
93 set(score, index);
94 }
95 }
96
97 /**
98 * Provides comaprison for bytes[].
99 *
100 * <p> Comparison does as follows. If the first value of {@code left} is larger than the
101 * first value of {@code right}, {@code left} is large than {@code right}. If the first
102 * value of {@code left} is less than the first value of {@code right}, {@code left} is less
103 * than {@code right}. If the first value of {@code left} and the first value of
104 * {@code right} is equal, do the same comparison to the next value. Finally if all values
105 * in {@code left} and {@code right} are equal, {@code left} and {@code right} is equal.</p>
106 *
107 * @param left The length must be equal to {@code right}.
108 * @param right The length must be equal to {@code left}.
109 * @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than
110 * {@code right}. 0 if {@code left} and {@code right} is equal.
111 */
112 @IntRange(from=-1, to=1)
113 private static int compare(@NonNull byte[] left, @NonNull byte[] right) {
114 for (int i = 0; i < left.length; ++i) {
115 if (left[i] > right[i]) {
116 return 1;
117 } else if (left[i] < right[i]) {
118 return -1;
119 }
120 }
121 return 0;
122 }
123
124 @Override
125 public int compareTo(final ScoreEntry other) {
126 return -1 * compare(mScore, other.mScore); // Order by descending order.
127 }
Yohei Yukawa102ff072016-02-24 18:25:16 -0800128 }
129
130 /**
131 * Filters the given items based on language preferences.
132 *
Yohei Yukawadce7df32017-05-04 16:28:18 -0700133 * <p>For each language found in {@code preferredLocales}, this method tries to copy at most
Yohei Yukawa102ff072016-02-24 18:25:16 -0800134 * one best-match item from {@code source} to {@code dest}. For example, if
Yohei Yukawadce7df32017-05-04 16:28:18 -0700135 * {@code "en-GB", "ja", "en-AU", "fr-CA", "en-IN"} is specified to {@code preferredLocales},
Yohei Yukawa102ff072016-02-24 18:25:16 -0800136 * this method tries to copy at most one English locale, at most one Japanese, and at most one
137 * French locale from {@code source} to {@code dest}. Here the best matching English locale
Seigo Nonaka072a95a2016-03-04 18:38:26 -0800138 * will be searched from {@code source} based on matching score. For the score design, see
Yohei Yukawadce7df32017-05-04 16:28:18 -0700139 * {@link LocaleUtils#calculateMatchingSubScore(ULocale, ULocale)}</p>
Yohei Yukawa102ff072016-02-24 18:25:16 -0800140 *
141 * @param sources Source items to be filtered.
142 * @param extractor Type converter from the source items to {@link Locale} object.
Yohei Yukawadce7df32017-05-04 16:28:18 -0700143 * @param preferredLocales Ordered list of locales with which the input items will be
Yohei Yukawa102ff072016-02-24 18:25:16 -0800144 * filtered.
145 * @param dest Destination into which the filtered items will be added.
146 * @param <T> Type of the data items.
147 */
Yohei Yukawa102ff072016-02-24 18:25:16 -0800148 public static <T> void filterByLanguage(
149 @NonNull List<T> sources,
150 @NonNull LocaleExtractor<T> extractor,
Yohei Yukawadce7df32017-05-04 16:28:18 -0700151 @NonNull LocaleList preferredLocales,
Yohei Yukawa102ff072016-02-24 18:25:16 -0800152 @NonNull ArrayList<T> dest) {
Yohei Yukawadce7df32017-05-04 16:28:18 -0700153 if (preferredLocales.isEmpty()) {
154 return;
155 }
156
157 final int numPreferredLocales = preferredLocales.size();
Yohei Yukawa7a712372018-11-20 18:46:05 -0800158 final ArrayMap<String, ScoreEntry> scoreboard = new ArrayMap<>();
Yohei Yukawadce7df32017-05-04 16:28:18 -0700159 final byte[] score = new byte[numPreferredLocales];
160 final ULocale[] preferredULocaleCache = new ULocale[numPreferredLocales];
Seigo Nonaka072a95a2016-03-04 18:38:26 -0800161
162 final int sourceSize = sources.size();
163 for (int i = 0; i < sourceSize; ++i) {
164 final Locale locale = extractor.get(sources.get(i));
Yohei Yukawadce7df32017-05-04 16:28:18 -0700165 if (locale == null) {
166 continue;
167 }
168
169 boolean canSkip = true;
170 for (int j = 0; j < numPreferredLocales; ++j) {
171 final Locale preferredLocale = preferredLocales.get(j);
172 if (!TextUtils.equals(locale.getLanguage(), preferredLocale.getLanguage())) {
173 score[j] = 0;
174 continue;
175 }
176 if (preferredULocaleCache[j] == null) {
177 preferredULocaleCache[j] = ULocale.addLikelySubtags(
178 ULocale.forLocale(preferredLocale));
179 }
180 score[j] = calculateMatchingSubScore(
181 preferredULocaleCache[j],
182 ULocale.addLikelySubtags(ULocale.forLocale(locale)));
183 if (canSkip && score[j] != 0) {
184 canSkip = false;
185 }
186 }
187 if (canSkip) {
Seigo Nonaka072a95a2016-03-04 18:38:26 -0800188 continue;
189 }
190
191 final String lang = locale.getLanguage();
192 final ScoreEntry bestScore = scoreboard.get(lang);
193 if (bestScore == null) {
194 scoreboard.put(lang, new ScoreEntry(score, i));
195 } else {
196 bestScore.updateIfBetter(score, i);
Yohei Yukawa102ff072016-02-24 18:25:16 -0800197 }
198 }
199
Yohei Yukawa7a712372018-11-20 18:46:05 -0800200 final int numEntries = scoreboard.size();
201 final ScoreEntry[] result = new ScoreEntry[numEntries];
202 for (int i = 0; i < numEntries; ++i) {
203 result[i] = scoreboard.valueAt(i);
204 }
Seigo Nonaka072a95a2016-03-04 18:38:26 -0800205 Arrays.sort(result);
206 for (final ScoreEntry entry : result) {
207 dest.add(sources.get(entry.mIndex));
Yohei Yukawa102ff072016-02-24 18:25:16 -0800208 }
209 }
Seigo Nonaka072a95a2016-03-04 18:38:26 -0800210}