blob: 773293f8332392438566fe6ad8f186062f5f7cab [file] [log] [blame]
/*
* Copyright (C) 2013 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.server.inputmethod;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserHandleAware;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.Build;
import android.os.LocaleList;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
import android.util.Pair;
import android.util.Printer;
import android.util.Slog;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.textservice.SpellCheckerInfo;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.StartInputFlags;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.textservices.TextServicesManagerInternal;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* This class provides random static utility methods for {@link InputMethodManagerService} and its
* utility classes.
*
* <p>This class is intentionally package-private. Utility methods here are tightly coupled with
* implementation details in {@link InputMethodManagerService}. Hence this class is not suitable
* for other components to directly use.</p>
*/
final class InputMethodUtils {
public static final boolean DEBUG = false;
static final int NOT_A_SUBTYPE_ID = -1;
private static final String TAG = "InputMethodUtils";
private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
// The string for enabled input method is saved as follows:
// example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
private static final char INPUT_METHOD_SEPARATOR = ':';
private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
private InputMethodUtils() {
// This utility class is not publicly instantiable.
}
// ----------------------------------------------------------------------
static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
if (subtype == null) return true;
return !subtype.isAuxiliary();
}
@UserHandleAware
static void setNonSelectedSystemImesDisabledUntilUsed(PackageManager packageManagerForUser,
List<InputMethodInfo> enabledImis) {
if (DEBUG) {
Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
}
final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
return;
}
// Only the current spell checker should be treated as an enabled one.
final SpellCheckerInfo currentSpellChecker =
TextServicesManagerInternal.get().getCurrentSpellCheckerForUser(
packageManagerForUser.getUserId());
for (final String packageName : systemImesDisabledUntilUsed) {
if (DEBUG) {
Slog.d(TAG, "check " + packageName);
}
boolean enabledIme = false;
for (int j = 0; j < enabledImis.size(); ++j) {
final InputMethodInfo imi = enabledImis.get(j);
if (packageName.equals(imi.getPackageName())) {
enabledIme = true;
break;
}
}
if (enabledIme) {
// enabled ime. skip
continue;
}
if (currentSpellChecker != null
&& packageName.equals(currentSpellChecker.getPackageName())) {
// enabled spell checker. skip
if (DEBUG) {
Slog.d(TAG, packageName + " is the current spell checker. skip");
}
continue;
}
ApplicationInfo ai;
try {
ai = packageManagerForUser.getApplicationInfo(packageName,
PackageManager.ApplicationInfoFlags.of(
PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS));
} catch (PackageManager.NameNotFoundException e) {
// This is not an error. No need to show scary error messages.
if (DEBUG) {
Slog.d(TAG, packageName
+ " does not exist for userId=" + packageManagerForUser.getUserId());
}
continue;
}
if (ai == null) {
// No app found for packageName
continue;
}
final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
if (!isSystemPackage) {
continue;
}
setDisabledUntilUsed(packageManagerForUser, packageName);
}
}
private static void setDisabledUntilUsed(PackageManager packageManagerForUser,
String packageName) {
final int state;
try {
state = packageManagerForUser.getApplicationEnabledSetting(packageName);
} catch (IllegalArgumentException e) {
Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
+ " userId=" + packageManagerForUser.getUserId(), e);
return;
}
if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|| state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
if (DEBUG) {
Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
}
try {
packageManagerForUser.setApplicationEnabledSetting(packageName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
0 /* newState */);
} catch (IllegalArgumentException e) {
Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
+ " userId=" + packageManagerForUser.getUserId(), e);
return;
}
} else {
if (DEBUG) {
Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
}
}
}
/**
* Returns true if a package name belongs to a UID.
*
* <p>This is a simple wrapper of
* {@link PackageManagerInternal#getPackageUid(String, long, int)}.</p>
* @param packageManagerInternal the {@link PackageManagerInternal} object to be used for the
* validation.
* @param uid the UID to be validated.
* @param packageName the package name.
* @return {@code true} if the package name belongs to the UID.
*/
static boolean checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal,
int uid, String packageName) {
// PackageManagerInternal#getPackageUid() doesn't check MATCH_INSTANT/MATCH_APEX as of
// writing. So setting 0 should be fine.
return packageManagerInternal.isSameApp(packageName, /* flags= */ 0, uid,
UserHandle.getUserId(uid));
}
/**
* Utility class for putting and getting settings for InputMethod.
*
* This is used in two ways:
* - Singleton instance in {@link InputMethodManagerService}, which is updated on user-switch to
* follow the current user.
* - On-demand instances when we need settings for non-current users.
*
* TODO: Move all putters and getters of settings to this class.
*/
@UserHandleAware
public static class InputMethodSettings {
private final ArrayMap<String, InputMethodInfo> mMethodMap;
private boolean mCopyOnWrite = false;
@NonNull
private String mEnabledInputMethodsStrCache = "";
@UserIdInt
private int mCurrentUserId;
private static void buildEnabledInputMethodsSettingString(
StringBuilder builder, Pair<String, ArrayList<String>> ime) {
builder.append(ime.first);
// Inputmethod and subtypes are saved in the settings as follows:
// ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
for (String subtypeId: ime.second) {
builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
}
}
private static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
String enabledInputMethodsStr,
TextUtils.SimpleStringSplitter inputMethodSplitter,
TextUtils.SimpleStringSplitter subtypeSplitter) {
ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
if (TextUtils.isEmpty(enabledInputMethodsStr)) {
return imsList;
}
inputMethodSplitter.setString(enabledInputMethodsStr);
while (inputMethodSplitter.hasNext()) {
String nextImsStr = inputMethodSplitter.next();
subtypeSplitter.setString(nextImsStr);
if (subtypeSplitter.hasNext()) {
ArrayList<String> subtypeHashes = new ArrayList<>();
// The first element is ime id.
String imeId = subtypeSplitter.next();
while (subtypeSplitter.hasNext()) {
subtypeHashes.add(subtypeSplitter.next());
}
imsList.add(new Pair<>(imeId, subtypeHashes));
}
}
return imsList;
}
InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId,
boolean copyOnWrite) {
mMethodMap = methodMap;
switchCurrentUser(userId, copyOnWrite);
}
/**
* Must be called when the current user is changed.
*
* @param userId The user ID.
* @param copyOnWrite If {@code true}, for each settings key
* (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
* settings on the {@link Settings.Secure} until we do the first write operation.
*/
void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
if (DEBUG) {
Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
}
if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
mEnabledInputMethodsStrCache = "";
}
mCurrentUserId = userId;
mCopyOnWrite = copyOnWrite;
}
private void putString(@NonNull String key, @Nullable String str) {
SecureSettingsWrapper.putString(key, str, mCurrentUserId);
}
@Nullable
private String getString(@NonNull String key, @Nullable String defaultValue) {
return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId);
}
private void putInt(String key, int value) {
SecureSettingsWrapper.putInt(key, value, mCurrentUserId);
}
private int getInt(String key, int defaultValue) {
return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId);
}
private void putBoolean(String key, boolean value) {
SecureSettingsWrapper.putBoolean(key, value, mCurrentUserId);
}
private boolean getBoolean(String key, boolean defaultValue) {
return SecureSettingsWrapper.getBoolean(key, defaultValue, mCurrentUserId);
}
ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
}
@NonNull
ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked(
@Nullable Predicate<InputMethodInfo> matchingCondition) {
return createEnabledInputMethodListLocked(
getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition);
}
List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) {
List<InputMethodSubtype> enabledSubtypes =
getEnabledInputMethodSubtypeListLocked(imi);
if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) {
enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
SystemLocaleWrapper.get(mCurrentUserId), imi);
}
return InputMethodSubtype.sort(imi, enabledSubtypes);
}
List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
List<Pair<String, ArrayList<String>>> imsList =
getEnabledInputMethodsAndSubtypeListLocked();
ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
if (imi != null) {
for (Pair<String, ArrayList<String>> imsPair : imsList) {
InputMethodInfo info = mMethodMap.get(imsPair.first);
if (info != null && info.getId().equals(imi.getId())) {
final int subtypeCount = info.getSubtypeCount();
for (int i = 0; i < subtypeCount; ++i) {
InputMethodSubtype ims = info.getSubtypeAt(i);
for (String s: imsPair.second) {
if (String.valueOf(ims.hashCode()).equals(s)) {
enabledSubtypes.add(ims);
}
}
}
break;
}
}
}
return enabledSubtypes;
}
List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR),
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR));
}
List<String> getEnabledInputMethodNames() {
List<String> result = new ArrayList<>();
for (Pair<String, ArrayList<String>> pair :
getEnabledInputMethodsAndSubtypeListLocked()) {
result.add(pair.first);
}
return result;
}
void appendAndPutEnabledInputMethodLocked(String id) {
if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
// Add in the newly enabled input method.
putEnabledInputMethodsStr(id);
} else {
putEnabledInputMethodsStr(
mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
}
}
/**
* Build and put a string of EnabledInputMethods with removing specified Id.
* @return the specified id was removed or not.
*/
boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
boolean isRemoved = false;
boolean needsAppendSeparator = false;
for (Pair<String, ArrayList<String>> ims: imsList) {
String curId = ims.first;
if (curId.equals(id)) {
// We are disabling this input method, and it is
// currently enabled. Skip it to remove from the
// new list.
isRemoved = true;
} else {
if (needsAppendSeparator) {
builder.append(INPUT_METHOD_SEPARATOR);
} else {
needsAppendSeparator = true;
}
buildEnabledInputMethodsSettingString(builder, ims);
}
}
if (isRemoved) {
// Update the setting with the new list of input methods.
putEnabledInputMethodsStr(builder.toString());
}
return isRemoved;
}
private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
List<Pair<String, ArrayList<String>>> imsList,
Predicate<InputMethodInfo> matchingCondition) {
final ArrayList<InputMethodInfo> res = new ArrayList<>();
for (Pair<String, ArrayList<String>> ims: imsList) {
InputMethodInfo info = mMethodMap.get(ims.first);
if (info != null && !info.isVrOnly()
&& (matchingCondition == null || matchingCondition.test(info))) {
res.add(info);
}
}
return res;
}
void putEnabledInputMethodsStr(@Nullable String str) {
if (DEBUG) {
Slog.d(TAG, "putEnabledInputMethodStr: " + str);
}
if (TextUtils.isEmpty(str)) {
// OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
// empty data scenario.
putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
} else {
putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
}
// TODO: Update callers of putEnabledInputMethodsStr to make str @NonNull.
mEnabledInputMethodsStrCache = (str != null ? str : "");
}
@NonNull
String getEnabledInputMethodsStr() {
mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
if (DEBUG) {
Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
+ ", " + mCurrentUserId);
}
return mEnabledInputMethodsStrCache;
}
private void saveSubtypeHistory(
List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
StringBuilder builder = new StringBuilder();
boolean isImeAdded = false;
if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
newSubtypeId);
isImeAdded = true;
}
for (Pair<String, String> ime: savedImes) {
String imeId = ime.first;
String subtypeId = ime.second;
if (TextUtils.isEmpty(subtypeId)) {
subtypeId = NOT_A_SUBTYPE_ID_STR;
}
if (isImeAdded) {
builder.append(INPUT_METHOD_SEPARATOR);
} else {
isImeAdded = true;
}
builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
subtypeId);
}
// Remove the last INPUT_METHOD_SEPARATOR
putSubtypeHistoryStr(builder.toString());
}
private void addSubtypeToHistory(String imeId, String subtypeId) {
List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
for (Pair<String, String> ime: subtypeHistory) {
if (ime.first.equals(imeId)) {
if (DEBUG) {
Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
+ ime.second);
}
// We should break here
subtypeHistory.remove(ime);
break;
}
}
if (DEBUG) {
Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
}
saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
}
private void putSubtypeHistoryStr(@NonNull String str) {
if (DEBUG) {
Slog.d(TAG, "putSubtypeHistoryStr: " + str);
}
if (TextUtils.isEmpty(str)) {
// OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
// data scenario.
putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
} else {
putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
}
}
Pair<String, String> getLastInputMethodAndSubtypeLocked() {
// Gets the first one from the history
return getLastSubtypeForInputMethodLockedInternal(null);
}
@Nullable
InputMethodSubtype getLastInputMethodSubtypeLocked() {
final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked();
// TODO: Handle the case of the last IME with no subtypes
if (lastIme == null || TextUtils.isEmpty(lastIme.first)
|| TextUtils.isEmpty(lastIme.second)) return null;
final InputMethodInfo lastImi = mMethodMap.get(lastIme.first);
if (lastImi == null) return null;
try {
final int lastSubtypeHash = Integer.parseInt(lastIme.second);
final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi,
lastSubtypeHash);
if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) {
return null;
}
return lastImi.getSubtypeAt(lastSubtypeId);
} catch (NumberFormatException e) {
return null;
}
}
String getLastSubtypeForInputMethodLocked(String imeId) {
Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
if (ime != null) {
return ime.second;
} else {
return null;
}
}
private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
List<Pair<String, ArrayList<String>>> enabledImes =
getEnabledInputMethodsAndSubtypeListLocked();
List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
for (Pair<String, String> imeAndSubtype : subtypeHistory) {
final String imeInTheHistory = imeAndSubtype.first;
// If imeId is empty, returns the first IME and subtype in the history
if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
final String subtypeInTheHistory = imeAndSubtype.second;
final String subtypeHashCode =
getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
enabledImes, imeInTheHistory, subtypeInTheHistory);
if (!TextUtils.isEmpty(subtypeHashCode)) {
if (DEBUG) {
Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
}
return new Pair<>(imeInTheHistory, subtypeHashCode);
}
}
}
if (DEBUG) {
Slog.d(TAG, "No enabled IME found in the history");
}
return null;
}
private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId);
for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
if (enabledIme.first.equals(imeId)) {
final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
final InputMethodInfo imi = mMethodMap.get(imeId);
if (explicitlyEnabledSubtypes.size() == 0) {
// If there are no explicitly enabled subtypes, applicable subtypes are
// enabled implicitly.
// If IME is enabled and no subtypes are enabled, applicable subtypes
// are enabled implicitly, so needs to treat them to be enabled.
if (imi != null && imi.getSubtypeCount() > 0) {
List<InputMethodSubtype> implicitlyEnabledSubtypes =
SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList,
imi);
final int numSubtypes = implicitlyEnabledSubtypes.size();
for (int i = 0; i < numSubtypes; ++i) {
final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i);
if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
return subtypeHashCode;
}
}
}
} else {
for (String s: explicitlyEnabledSubtypes) {
if (s.equals(subtypeHashCode)) {
// If both imeId and subtypeId are enabled, return subtypeId.
try {
final int hashCode = Integer.parseInt(subtypeHashCode);
// Check whether the subtype id is valid or not
if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) {
return s;
} else {
return NOT_A_SUBTYPE_ID_STR;
}
} catch (NumberFormatException e) {
return NOT_A_SUBTYPE_ID_STR;
}
}
}
}
// If imeId was enabled but subtypeId was disabled.
return NOT_A_SUBTYPE_ID_STR;
}
}
// If both imeId and subtypeId are disabled, return null
return null;
}
private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
ArrayList<Pair<String, String>> imsList = new ArrayList<>();
final String subtypeHistoryStr = getSubtypeHistoryStr();
if (TextUtils.isEmpty(subtypeHistoryStr)) {
return imsList;
}
final TextUtils.SimpleStringSplitter inputMethodSplitter =
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
final TextUtils.SimpleStringSplitter subtypeSplitter =
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
inputMethodSplitter.setString(subtypeHistoryStr);
while (inputMethodSplitter.hasNext()) {
String nextImsStr = inputMethodSplitter.next();
subtypeSplitter.setString(nextImsStr);
if (subtypeSplitter.hasNext()) {
String subtypeId = NOT_A_SUBTYPE_ID_STR;
// The first element is ime id.
String imeId = subtypeSplitter.next();
while (subtypeSplitter.hasNext()) {
subtypeId = subtypeSplitter.next();
break;
}
imsList.add(new Pair<>(imeId, subtypeId));
}
}
return imsList;
}
@NonNull
private String getSubtypeHistoryStr() {
final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
if (DEBUG) {
Slog.d(TAG, "getSubtypeHistoryStr: " + history);
}
return history;
}
void putSelectedInputMethod(String imeId) {
if (DEBUG) {
Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
+ mCurrentUserId);
}
putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
}
void putSelectedSubtype(int subtypeId) {
if (DEBUG) {
Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
+ mCurrentUserId);
}
putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
}
@Nullable
String getSelectedInputMethod() {
final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
if (DEBUG) {
Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
}
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;
}
private int getSelectedInputMethodSubtypeHashCode() {
return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
}
boolean isShowImeWithHardKeyboardEnabled() {
return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
}
void setShowImeWithHardKeyboard(boolean show) {
putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
}
@UserIdInt
public int getCurrentUserId() {
return mCurrentUserId;
}
int getSelectedInputMethodSubtypeId(String selectedImiId) {
final InputMethodInfo imi = mMethodMap.get(selectedImiId);
if (imi == null) {
return NOT_A_SUBTYPE_ID;
}
final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode);
}
void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId,
InputMethodSubtype currentSubtype) {
String subtypeId = NOT_A_SUBTYPE_ID_STR;
if (currentSubtype != null) {
subtypeId = String.valueOf(currentSubtype.hashCode());
}
if (canAddToLastInputMethod(currentSubtype)) {
addSubtypeToHistory(curMethodId, subtypeId);
}
}
/**
* A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for
* non-current users.
*
* <p>TODO: Address code duplication between this and
* {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p>
*
* @return {@link InputMethodSubtype} if exists. {@code null} otherwise.
*/
@Nullable
InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() {
final String selectedMethodId = getSelectedInputMethod();
if (selectedMethodId == null) {
return null;
}
final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
if (imi == null || imi.getSubtypeCount() == 0) {
return null;
}
final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
if (subtypeHashCode != InputMethodUtils.NOT_A_SUBTYPE_ID) {
final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi,
subtypeHashCode);
if (subtypeIndex >= 0) {
return imi.getSubtypeAt(subtypeIndex);
}
}
// If there are no selected subtypes, the framework will try to find the most applicable
// subtype from explicitly or implicitly enabled subtypes.
final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
getEnabledInputMethodSubtypeListLocked(imi, true);
// If there is only one explicitly or implicitly enabled subtype, just returns it.
if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) {
return null;
}
if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
return explicitlyOrImplicitlyEnabledSubtypes.get(0);
}
final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString();
final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD,
locale, true);
if (subtype != null) {
return subtype;
}
return SubtypeUtils.findLastResortApplicableSubtypeLocked(
explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
}
boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
@NonNull ArrayList<InputMethodSubtype> subtypes,
@NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
@NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
final InputMethodInfo imi = mMethodMap.get(imeId);
if (imi == null) {
return false;
}
if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
imi.getPackageName())) {
return false;
}
if (subtypes.isEmpty()) {
additionalSubtypeMap.remove(imi.getId());
} else {
additionalSubtypeMap.put(imi.getId(), subtypes);
}
AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId());
return true;
}
boolean setEnabledInputMethodSubtypes(@NonNull String imeId,
@NonNull int[] subtypeHashCodes) {
final InputMethodInfo imi = mMethodMap.get(imeId);
if (imi == null) {
return false;
}
final IntArray validSubtypeHashCodes = new IntArray(subtypeHashCodes.length);
for (int subtypeHashCode : subtypeHashCodes) {
if (subtypeHashCode == NOT_A_SUBTYPE_ID) {
continue; // NOT_A_SUBTYPE_ID must not be saved
}
if (!SubtypeUtils.isValidSubtypeId(imi, subtypeHashCode)) {
continue; // this subtype does not exist in InputMethodInfo.
}
if (validSubtypeHashCodes.indexOf(subtypeHashCode) >= 0) {
continue; // The entry is already added. No need to add anymore.
}
validSubtypeHashCodes.add(subtypeHashCode);
}
final String originalEnabledImesString = getEnabledInputMethodsStr();
final String updatedEnabledImesString = updateEnabledImeString(
originalEnabledImesString, imi.getId(), validSubtypeHashCodes);
if (TextUtils.equals(originalEnabledImesString, updatedEnabledImesString)) {
return false;
}
putEnabledInputMethodsStr(updatedEnabledImesString);
return true;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static String updateEnabledImeString(@NonNull String enabledImesString,
@NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes) {
final TextUtils.SimpleStringSplitter imeSplitter =
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
final StringBuilder sb = new StringBuilder();
imeSplitter.setString(enabledImesString);
boolean needsImeSeparator = false;
while (imeSplitter.hasNext()) {
final String nextImsStr = imeSplitter.next();
imeSubtypeSplitter.setString(nextImsStr);
if (imeSubtypeSplitter.hasNext()) {
if (needsImeSeparator) {
sb.append(INPUT_METHOD_SEPARATOR);
}
if (TextUtils.equals(imeId, imeSubtypeSplitter.next())) {
sb.append(imeId);
for (int i = 0; i < enabledSubtypeHashCodes.size(); ++i) {
sb.append(INPUT_METHOD_SUBTYPE_SEPARATOR);
sb.append(enabledSubtypeHashCodes.get(i));
}
} else {
sb.append(nextImsStr);
}
needsImeSeparator = true;
}
}
return sb.toString();
}
public void dumpLocked(final Printer pw, final String prefix) {
pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
}
}
static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
@StartInputFlags int startInputFlags) {
if (targetSdkVersion < Build.VERSION_CODES.P) {
// for compatibility.
return true;
}
if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) {
return false;
}
if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) {
return false;
}
return true;
}
/**
* Converts a user ID, which can be a pseudo user ID such as {@link UserHandle#USER_ALL} to a
* list of real user IDs.
*
* @param userIdToBeResolved A user ID. Two pseudo user ID {@link UserHandle#USER_CURRENT} and
* {@link UserHandle#USER_ALL} are also supported
* @param currentUserId A real user ID, which will be used when {@link UserHandle#USER_CURRENT}
* is specified in {@code userIdToBeResolved}.
* @param warningWriter A {@link PrintWriter} to output some debug messages. {@code null} if
* no debug message is required.
* @return An integer array that contain user IDs.
*/
static int[] resolveUserId(@UserIdInt int userIdToBeResolved,
@UserIdInt int currentUserId, @Nullable PrintWriter warningWriter) {
final UserManagerInternal userManagerInternal =
LocalServices.getService(UserManagerInternal.class);
if (userIdToBeResolved == UserHandle.USER_ALL) {
return userManagerInternal.getUserIds();
}
final int sourceUserId;
if (userIdToBeResolved == UserHandle.USER_CURRENT) {
sourceUserId = currentUserId;
} else if (userIdToBeResolved < 0) {
if (warningWriter != null) {
warningWriter.print("Pseudo user ID ");
warningWriter.print(userIdToBeResolved);
warningWriter.println(" is not supported.");
}
return new int[]{};
} else if (userManagerInternal.exists(userIdToBeResolved)) {
sourceUserId = userIdToBeResolved;
} else {
if (warningWriter != null) {
warningWriter.print("User #");
warningWriter.print(userIdToBeResolved);
warningWriter.println(" does not exit.");
}
return new int[]{};
}
return new int[]{sourceUserId};
}
/**
* Returns a list of enabled IME IDs to address Bug 261723412.
*
* <p>This is a temporary workaround until we come up with a better solution. Do not use this
* for anything other than Bug 261723412.</p>
*
* @param context {@link Context} object to query secure settings.
* @param userId User ID to query about.
* @return A list of enabled IME IDs.
*/
@NonNull
static List<String> getEnabledInputMethodIdsForFiltering(@NonNull Context context,
@UserIdInt int userId) {
final String enabledInputMethodsStr = TextUtils.nullIfEmpty(
SecureSettingsWrapper.getString(Settings.Secure.ENABLED_INPUT_METHODS, null,
userId));
final ArrayList<String> result = new ArrayList<>();
splitEnabledImeStr(enabledInputMethodsStr, result::add);
return result;
}
/**
* Split enabled IME string ({@link Settings.Secure#ENABLED_INPUT_METHODS}) into IME IDs.
*
* @param text a text formatted with {@link Settings.Secure#ENABLED_INPUT_METHODS}.
* @param consumer {@link Consumer} called back when a new IME ID is found.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static void splitEnabledImeStr(@Nullable String text, @NonNull Consumer<String> consumer) {
if (TextUtils.isEmpty(text)) {
return;
}
final TextUtils.SimpleStringSplitter inputMethodSplitter =
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
final TextUtils.SimpleStringSplitter subtypeSplitter =
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
inputMethodSplitter.setString(text);
while (inputMethodSplitter.hasNext()) {
String nextImsStr = inputMethodSplitter.next();
subtypeSplitter.setString(nextImsStr);
if (subtypeSplitter.hasNext()) {
// The first element is ime id.
consumer.accept(subtypeSplitter.next());
}
}
}
/**
* Concat given IME IDs with an existing enabled IME
* ({@link Settings.Secure#ENABLED_INPUT_METHODS}).
*
* @param existingEnabledImeId an existing {@link Settings.Secure#ENABLED_INPUT_METHODS} to
* which {@code imeIDs} will be added.
* @param imeIds an array of IME IDs to be added. For IME IDs that are already seen in
* {@code existingEnabledImeId} will be skipped.
* @return a new enabled IME ID string that can be stored in
* {@link Settings.Secure#ENABLED_INPUT_METHODS}.
*/
@NonNull
static String concatEnabledImeIds(@NonNull String existingEnabledImeId,
@NonNull String... imeIds) {
final ArraySet<String> alreadyEnabledIds = new ArraySet<>();
final StringJoiner joiner = new StringJoiner(Character.toString(INPUT_METHOD_SEPARATOR));
if (!TextUtils.isEmpty(existingEnabledImeId)) {
splitEnabledImeStr(existingEnabledImeId, alreadyEnabledIds::add);
joiner.add(existingEnabledImeId);
}
for (String id : imeIds) {
if (!alreadyEnabledIds.contains(id)) {
joiner.add(id);
alreadyEnabledIds.add(id);
}
}
return joiner.toString();
}
/**
* Convert the input method ID to a component name
*
* @param id A unique ID for this input method.
* @return The component name of the input method.
* @see InputMethodInfo#computeId(ResolveInfo)
*/
@Nullable
public static ComponentName convertIdToComponentName(@NonNull String id) {
return ComponentName.unflattenFromString(id);
}
}