diff options
6 files changed, 227 insertions, 0 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 719c38392fb1..37469af1aa24 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6291,6 +6291,20 @@ public final class Settings { "selected_input_method_subtype"; /** + * The {@link android.view.inputmethod.InputMethodInfo.InputMethodInfo#getId() ID} of the + * default voice input method. + * <p> + * This stores the last known default voice IME. If the related system config value changes, + * this is reset by InputMethodManagerService. + * <p> + * This IME is not necessarily in the enabled IME list. That state is still stored in + * {@link #ENABLED_INPUT_METHODS}. + * + * @hide + */ + public static final String DEFAULT_VOICE_INPUT_METHOD = "default_voice_input_method"; + + /** * Setting to record the history of input method subtype, holding the pair of ID of IME * and its last used subtype. * @hide diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index dca6002d23f8..f0badbefcfeb 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -296,6 +296,7 @@ message SecureSettingsProto { optional SettingProto subtype_history = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto selected_input_method_subtype = 6 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto show_ime_with_hard_keyboard = 7 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto default_voice_input_method = 8 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional InputMethods input_methods = 26; diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 7288371899ce..9c67e9c342be 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2150,6 +2150,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, SecureSettingsProto.InputMethods.SHOW_IME_WITH_HARD_KEYBOARD); + dumpSetting(s, p, + Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, + SecureSettingsProto.InputMethods.DEFAULT_VOICE_INPUT_METHOD); p.end(inputMethodsToken); dumpSetting(s, p, diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index c9364c62d8b7..27f8fd3e8f5c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -4830,6 +4830,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub setInputMethodEnabledLocked(defaultImiId, true); } } + + updateDefaultVoiceImeIfNeededLocked(); + // Here is not the perfect place to reset the switching controller. Ideally // mSwitchingController and mSettings should be able to share the same state. // TODO: Make sure that mSwitchingController and mSettings are sharing the @@ -4842,6 +4845,37 @@ public class InputMethodManagerService extends IInputMethodManager.Stub mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget(); } + @GuardedBy("mMethodMap") + private void updateDefaultVoiceImeIfNeededLocked() { + final String systemSpeechRecognizer = + mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer); + final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod(); + final InputMethodInfo newSystemVoiceIme = InputMethodUtils.chooseSystemVoiceIme( + mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId); + if (newSystemVoiceIme == null) { + if (DEBUG) { + Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked," + + " this may be expected."); + } + // Clear DEFAULT_VOICE_INPUT_METHOD when necessary. Note that InputMethodSettings + // does not update the actual Secure Settings until the user is unlocked. + if (!TextUtils.isEmpty(currentDefaultVoiceImeId)) { + mSettings.putDefaultVoiceInputMethod(""); + // We don't support disabling the voice ime when a package is removed from the + // config. + } + return; + } + if (TextUtils.equals(currentDefaultVoiceImeId, newSystemVoiceIme.getId())) { + return; + } + if (DEBUG) { + Slog.i(TAG, "Enabling the default Voice IME:" + newSystemVoiceIme); + } + setInputMethodEnabledLocked(newSystemVoiceIme.getId(), true); + mSettings.putDefaultVoiceInputMethod(newSystemVoiceIme.getId()); + } + // ---------------------------------------------------------------------- private void showInputMethodAndSubtypeEnabler(String inputMethodId) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index 0e908d471f74..ac3c31d5cca4 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -337,6 +337,52 @@ final class InputMethodUtils { return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); } + /** + * Chooses an eligible system voice IME from the given IMEs. + * + * @param methodMap Map from the IME ID to {@link InputMethodInfo}. + * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system + * config. + * @param currentDefaultVoiceImeId IME ID currently set to + * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} + * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for + * the system voice IME. + */ + @Nullable + static InputMethodInfo chooseSystemVoiceIme( + @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @Nullable String systemSpeechRecognizerPackageName, + @Nullable String currentDefaultVoiceImeId) { + if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { + return null; + } + final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); + // If the config matches the package of the setting, use the current one. + if (defaultVoiceIme != null && defaultVoiceIme.isSystem() + && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { + return defaultVoiceIme; + } + InputMethodInfo firstMatchingIme = null; + final int methodCount = methodMap.size(); + for (int i = 0; i < methodCount; ++i) { + final InputMethodInfo imi = methodMap.valueAt(i); + if (!imi.isSystem()) { + continue; + } + if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { + continue; + } + if (firstMatchingIme != null) { + Slog.e(TAG, "At most one InputMethodService can be published in " + + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName + + ". Ignoring all of them."); + return null; + } + firstMatchingIme = imi; + } + return firstMatchingIme; + } + static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, boolean checkCountry, String mode) { if (locale == null) { @@ -1233,6 +1279,22 @@ final class InputMethodUtils { return imi; } + void putDefaultVoiceInputMethod(String imeId) { + if (DEBUG) { + Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId); + } + putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId); + } + + @Nullable + String getDefaultVoiceInputMethod() { + final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null); + if (DEBUG) { + Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi); + } + return imi; + } + boolean isSubtypeSelected() { return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID; } diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index eebc25aab279..6f1268e5de24 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -23,6 +23,7 @@ import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.content.Context; @@ -34,6 +35,7 @@ import android.content.res.Resources; import android.os.Build; import android.os.LocaleList; import android.os.Parcel; +import android.util.ArrayMap; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; @@ -48,6 +50,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -793,6 +796,97 @@ public class InputMethodUtilsTest { } } + @Test + public void testChooseSystemVoiceIme() throws Exception { + final InputMethodInfo systemIme = createFakeInputMethodInfo("SystemIme", "fake.voice0", + true /* isSystem */); + + // Returns null when the config value is null. + { + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + methodMap.put(systemIme.getId(), systemIme); + assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, null, "")); + } + + // Returns null when the config value is empty. + { + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + methodMap.put(systemIme.getId(), systemIme); + assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, "", "")); + } + + // Returns null when the configured package doesn't have an IME. + { + assertNull(InputMethodUtils.chooseSystemVoiceIme(new ArrayMap<>(), + systemIme.getPackageName(), "")); + } + + // Returns the right one when the current default is null. + { + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + methodMap.put(systemIme.getId(), systemIme); + assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + systemIme.getPackageName(), null)); + } + + // Returns the right one when the current default is empty. + { + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + methodMap.put(systemIme.getId(), systemIme); + assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + systemIme.getPackageName(), "")); + } + + // Returns null when the current default isn't found. + { + assertNull(InputMethodUtils.chooseSystemVoiceIme(new ArrayMap<>(), + systemIme.getPackageName(), systemIme.getId())); + } + + // Returns null when there are multiple IMEs defined by the config package. + { + final InputMethodInfo secondIme = createFakeInputMethodInfo(systemIme.getPackageName(), + "fake.voice1", true /* isSystem */); + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + methodMap.put(systemIme.getId(), systemIme); + methodMap.put(secondIme.getId(), secondIme); + assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), + "")); + } + + // Returns the current one when the current default and config point to the same package. + { + final InputMethodInfo secondIme = createFakeInputMethodInfo("SystemIme", "fake.voice1", + true /* isSystem */); + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + methodMap.put(systemIme.getId(), systemIme); + methodMap.put(secondIme.getId(), secondIme); + assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + systemIme.getPackageName(), systemIme.getId())); + } + + // Doesn't return the current default if it isn't a system app. + { + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme", + "fake.voice0", false /* isSystem */); + methodMap.put(nonSystemIme.getId(), nonSystemIme); + assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, + nonSystemIme.getPackageName(), nonSystemIme.getId())); + } + + // Returns null if the configured one isn't a system app. + { + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); + final InputMethodInfo nonSystemIme = createFakeInputMethodInfo( + "FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */); + methodMap.put(systemIme.getId(), systemIme); + methodMap.put(nonSystemIme.getId(), nonSystemIme); + assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, + nonSystemIme.getPackageName(), "")); + } + } + private void assertDefaultEnabledImes(final ArrayList<InputMethodInfo> preinstalledImes, final Locale systemLocale, String... expectedImeNames) { final Context context = createTargetContextWithLocales(new LocaleList(systemLocale)); @@ -866,6 +960,25 @@ public class InputMethodUtilsTest { } private static InputMethodInfo createFakeInputMethodInfo(String packageName, String name, + boolean isSystem) { + final ResolveInfo ri = new ResolveInfo(); + final ServiceInfo si = new ServiceInfo(); + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ai.enabled = true; + if (isSystem) { + ai.flags |= ApplicationInfo.FLAG_SYSTEM; + } + si.applicationInfo = ai; + si.enabled = true; + si.packageName = packageName; + si.name = name; + si.exported = true; + ri.serviceInfo = si; + return new InputMethodInfo(ri, false, "", Collections.emptyList(), 1, true); + } + + private static InputMethodInfo createFakeInputMethodInfo(String packageName, String name, CharSequence label, boolean isAuxIme, boolean isDefault, List<InputMethodSubtype> subtypes) { final ResolveInfo ri = new ResolveInfo(); |