Merge changes Iaf179d60,I315cf372,I21d3c5cc into nyc-dev

* changes:
  Use LocaleList for implicitly enabled subtypes.
  Add a utility method to filter locales.
  Mechanical refactoring in InputMethodUtilsTest.
This commit is contained in:
Yohei Yukawa
2016-02-25 02:57:50 +00:00
committed by Android (Google) Code Review
5 changed files with 450 additions and 81 deletions

View File

@@ -32,6 +32,7 @@ import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LocaleList;
import android.util.Pair;
import android.util.Printer;
import android.util.Slog;
@@ -486,18 +487,29 @@ public class InputMethodUtils {
return NOT_A_SUBTYPE_ID;
}
private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
@Override
public Locale get(InputMethodSubtype source) {
return source != null ? source.getLocaleObject() : null;
}
};
@VisibleForTesting
public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
Resources res, InputMethodInfo imi) {
final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
final String systemLocale = res.getConfiguration().locale.toString();
final LocaleList systemLocales = res.getConfiguration().getLocales();
final String systemLocale = systemLocales.get(0).toString();
if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
final String systemLanguage = res.getConfiguration().locale.getLanguage();
final int numSubtypes = subtypes.size();
// Handle overridesImplicitlyEnabledSubtype mechanism.
final String systemLanguage = systemLocales.get(0).getLanguage();
final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new HashMap<>();
final int N = subtypes.size();
for (int i = 0; i < N; ++i) {
for (int i = 0; i < numSubtypes; ++i) {
// scan overriding implicitly enabled subtypes.
InputMethodSubtype subtype = subtypes.get(i);
final InputMethodSubtype subtype = subtypes.get(i);
if (subtype.overridesImplicitlyEnabledSubtype()) {
final String mode = subtype.getMode();
if (!applicableModeAndSubtypesMap.containsKey(mode)) {
@@ -508,42 +520,46 @@ public class InputMethodUtils {
if (applicableModeAndSubtypesMap.size() > 0) {
return new ArrayList<>(applicableModeAndSubtypesMap.values());
}
for (int i = 0; i < N; ++i) {
final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype subtype = subtypes.get(i);
final String locale = subtype.getLocale();
final String mode = subtype.getMode();
final String language = getLanguageFromLocaleString(locale);
// When system locale starts with subtype's locale, that subtype will be applicable
// for system locale. We need to make sure the languages are the same, to prevent
// locales like "fil" (Filipino) being matched by "fi" (Finnish).
//
// For instance, it's clearly applicable for cases like system locale = en_US and
// subtype = en, but it is not necessarily considered applicable for cases like system
// locale = en and subtype = en_US.
//
// We just call systemLocale.startsWith(locale) in this function because there is no
// need to find applicable subtypes aggressively unlike
// findLastResortApplicableSubtypeLocked.
//
// TODO: This check is broken. It won't take scripts into account and doesn't
// account for the mandatory conversions performed by Locale#toString.
if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) {
final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
// If more applicable subtypes are contained, skip.
if (applicableSubtype != null) {
if (systemLocale.equals(applicableSubtype.getLocale())) continue;
if (!systemLocale.equals(locale)) continue;
if (TextUtils.equals(SUBTYPE_MODE_KEYBOARD, subtype.getMode())) {
keyboardSubtypes.add(subtype);
} else {
final Locale locale = subtype.getLocaleObject();
final String mode = subtype.getMode();
// TODO: Take secondary system locales into consideration.
if (locale != null && locale.equals(systemLanguage)) {
final InputMethodSubtype applicableSubtype =
applicableModeAndSubtypesMap.get(mode);
// If more applicable subtypes are contained, skip.
if (applicableSubtype != null) {
if (systemLocale.equals(applicableSubtype.getLocaleObject())) continue;
if (!systemLocale.equals(locale)) continue;
}
applicableModeAndSubtypesMap.put(mode, subtype);
}
applicableModeAndSubtypesMap.put(mode, subtype);
}
}
final InputMethodSubtype keyboardSubtype
= applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(
applicableModeAndSubtypesMap.values());
if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
for (int i = 0; i < N; ++i) {
final InputMethodSubtype subtype = subtypes.get(i);
final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
applicableSubtypes);
boolean hasAsciiCapableKeyboard = false;
final int numApplicationSubtypes = applicableSubtypes.size();
for (int i = 0; i < numApplicationSubtypes; ++i) {
final InputMethodSubtype subtype = applicableSubtypes.get(i);
if (subtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
hasAsciiCapableKeyboard = true;
break;
}
}
if (!hasAsciiCapableKeyboard) {
final int numKeyboardSubtypes = keyboardSubtypes.size();
for (int i = 0; i < numKeyboardSubtypes; ++i) {
final InputMethodSubtype subtype = keyboardSubtypes.get(i);
final String mode = subtype.getMode();
if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
@@ -551,13 +567,16 @@ public class InputMethodUtils {
}
}
}
if (keyboardSubtype == null) {
if (applicableSubtypes.isEmpty()) {
InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
if (lastResortKeyboardSubtype != null) {
applicableSubtypes.add(lastResortKeyboardSubtype);
}
}
applicableSubtypes.addAll(applicableModeAndSubtypesMap.values());
return applicableSubtypes;
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.inputmethod;
import com.android.internal.annotations.VisibleForTesting;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.TextUtils;
import android.util.LocaleList;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public final class LocaleUtils {
@VisibleForTesting
public interface LocaleExtractor<T> {
@Nullable
Locale get(@Nullable T source);
}
@Nullable
private static String getLanguage(@Nullable Locale locale) {
if (locale == null) {
return null;
}
return locale.getLanguage();
}
/**
* Filters the given items based on language preferences.
*
* <p>For each language found in {@code preferredLanguages}, this method tries to copy at most
* one best-match item from {@code source} to {@code dest}. For example, if
* {@code "en-GB", "ja", "en-AU", "fr-CA", "en-IN"} is specified to {@code preferredLanguages},
* this method tries to copy at most one English locale, at most one Japanese, and at most one
* French locale from {@code source} to {@code dest}. Here the best matching English locale
* will be searched from {@code source} as follows.
* <ol>
* <li>The first instance in {@code sources} that exactly matches {@code "en-GB"}</li>
* <li>The first instance in {@code sources} that exactly matches {@code "en-AU"}</li>
* <li>The first instance in {@code sources} that exactly matches {@code "en-IN"}</li>
* <li>The first instance in {@code sources} that partially matches {@code "en"}</li>
* </ol>
* <p>Then this method iterates the same algorithm for Japanese then French.</p>
*
* @param sources Source items to be filtered.
* @param extractor Type converter from the source items to {@link Locale} object.
* @param preferredLanguages Ordered list of locales with which the input items will be
* filtered.
* @param dest Destination into which the filtered items will be added.
* @param <T> Type of the data items.
*/
@VisibleForTesting
public static <T> void filterByLanguage(
@NonNull List<T> sources,
@NonNull LocaleExtractor<T> extractor,
@NonNull LocaleList preferredLanguages,
@NonNull ArrayList<T> dest) {
final Locale[] availableLocales = new Locale[sources.size()];
for (int i = 0; i < availableLocales.length; ++i) {
availableLocales[i] = extractor.get(sources.get(i));
}
final Locale[] sortedPreferredLanguages = new Locale[preferredLanguages.size()];
if (sortedPreferredLanguages.length > 0) {
int nextIndex = 0;
final int N = preferredLanguages.size();
languageLoop:
for (int i = 0; i < N; ++i) {
final String language = getLanguage(preferredLanguages.get(i));
for (int j = 0; j < nextIndex; ++j) {
if (TextUtils.equals(getLanguage(sortedPreferredLanguages[j]), language)) {
continue languageLoop;
}
}
for (int j = i; j < N; ++j) {
final Locale locale = preferredLanguages.get(j);
if (TextUtils.equals(language, getLanguage(locale))) {
sortedPreferredLanguages[nextIndex] = locale;
++nextIndex;
}
}
}
}
for (int languageIndex = 0; languageIndex < sortedPreferredLanguages.length;) {
// Finding the range.
final String language = getLanguage(sortedPreferredLanguages[languageIndex]);
int nextLanguageIndex = languageIndex;
for (; nextLanguageIndex < sortedPreferredLanguages.length; ++nextLanguageIndex) {
final Locale locale = sortedPreferredLanguages[nextLanguageIndex];
if (!TextUtils.equals(getLanguage(locale), language)) {
break;
}
}
// Check exact match
boolean found = false;
for (int i = languageIndex; !found && i < nextLanguageIndex; ++i) {
final Locale locale = sortedPreferredLanguages[i];
for (int j = 0; j < availableLocales.length; ++j) {
if (!Objects.equals(locale, availableLocales[j])) {
continue;
}
dest.add(sources.get(j));
found = true;
break;
}
}
if (!found) {
// No exact match. Use language match.
for (int j = 0; j < availableLocales.length; ++j) {
if (!TextUtils.equals(language, getLanguage(availableLocales[j]))) {
continue;
}
dest.add(sources.get(j));
break;
}
}
languageIndex = nextLanguageIndex;
}
}
}