Merge "Introduce IMM#setExplicitlyEnabledInputMethodSubtypes()"

This commit is contained in:
TreeHugger Robot
2022-09-30 00:24:26 +00:00
committed by Android (Google) Code Review
7 changed files with 253 additions and 0 deletions

View File

@@ -53486,6 +53486,7 @@ package android.view.inputmethod {
method public void sendAppPrivateCommand(android.view.View, String, android.os.Bundle);
method @Deprecated public void setAdditionalInputMethodSubtypes(@NonNull String, @NonNull android.view.inputmethod.InputMethodSubtype[]);
method @Deprecated @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean setCurrentInputMethodSubtype(android.view.inputmethod.InputMethodSubtype);
method public void setExplicitlyEnabledInputMethodSubtypes(@NonNull String, @NonNull int[]);
method @Deprecated public void setInputMethod(android.os.IBinder, String);
method @Deprecated public void setInputMethodAndSubtype(@NonNull android.os.IBinder, String, android.view.inputmethod.InputMethodSubtype);
method @Deprecated public boolean shouldOfferSwitchingToNextInputMethod(android.os.IBinder);

View File

@@ -201,6 +201,16 @@ final class IInputMethodManagerInvoker {
}
}
@AnyThread
void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imeId,
@NonNull int[] subtypeHashCodes, @UserIdInt int userId) {
try {
mTarget.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@AnyThread
int getInputMethodWindowVisibleHeight(@NonNull IInputMethodClient client) {
try {

View File

@@ -3632,6 +3632,56 @@ public final class InputMethodManager {
mServiceInvoker.setAdditionalInputMethodSubtypes(imiId, subtypes, UserHandle.myUserId());
}
/**
* Updates the list of explicitly enabled {@link InputMethodSubtype} for a given IME owned by
* the calling process.
*
* <p>By default each IME has no explicitly enabled {@link InputMethodSubtype}. In this state
* the system will decide what {@link InputMethodSubtype} should be enabled by using information
* available at runtime as per-user language settings. Users can, however, manually pick up one
* or more {@link InputMethodSubtype} to be enabled on an Activity shown by
* {@link #showInputMethodAndSubtypeEnabler(String)}. Such a manual change is stored in
* {@link Settings.Secure#ENABLED_INPUT_METHODS} so that the change can persist across reboots.
* {@link Settings.Secure#ENABLED_INPUT_METHODS} stores {@link InputMethodSubtype#hashCode()} as
* the identifier of {@link InputMethodSubtype} for historical reasons.</p>
*
* <p>This API provides a safe and managed way for IME developers to modify what
* {@link InputMethodSubtype} are referenced in {@link Settings.Secure#ENABLED_INPUT_METHODS}
* for their own IME. One use case is when IME developers want to use their own Activity for
* users to pick up {@link InputMethodSubtype}. Another use case is for IME developers to fix up
* any stale and/or invalid value stored in {@link Settings.Secure#ENABLED_INPUT_METHODS}
* without bothering users. Passing an empty {@code subtypeHashCodes} is guaranteed to reset
* the state to default.</p>
*
* <h3>To control the return value of {@link InputMethodSubtype#hashCode()}</h3>
* <p>{@link android.R.attr#subtypeId} and {@link
* android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder#setSubtypeId(int)} are
* available for IME developers to control the return value of
* {@link InputMethodSubtype#hashCode()}. Beware that {@code -1} is not a valid value of
* {@link InputMethodSubtype#hashCode()} for historical reasons.</p>
*
* <h3>Note for Direct Boot support</h3>
* <p>While IME developers can call this API even before
* {@link android.os.UserManager#isUserUnlocked()} becomes {@code true}, such a change is
* volatile thus remains effective only until {@link android.os.UserManager#isUserUnlocked()}
* becomes {@code true} or the device is rebooted. To make the change persistent IME developers
* need to call this API again after receiving {@link Intent#ACTION_USER_UNLOCKED}.</p>
*
* @param imiId IME ID. The specified IME and the calling process need to belong to the same
* package. Otherwise {@link SecurityException} will be thrown.
* @param subtypeHashCodes An arrays of {@link InputMethodSubtype#hashCode()} to be explicitly
* enabled. Entries that are found in the specified IME will be silently
* ignored. Pass an empty array to reset the state to default.
* @throws NullPointerException if {@code subtypeHashCodes} is {@code null}.
* @throws SecurityException if the specified IME and the calling process do not belong to the
* same package.
*/
public void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imiId,
@NonNull int[] subtypeHashCodes) {
mServiceInvoker.setExplicitlyEnabledInputMethodSubtypes(imiId, subtypeHashCodes,
UserHandle.myUserId());
}
/**
* Returns the last used {@link InputMethodSubtype} in system history.
*

View File

@@ -97,6 +97,11 @@ interface IInputMethodManager {
void setAdditionalInputMethodSubtypes(String id, in InputMethodSubtype[] subtypes,
int userId);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
void setExplicitlyEnabledInputMethodSubtypes(String imeId, in int[] subtypeHashCodes,
int userId);
// This is kept due to @UnsupportedAppUsage.
// TODO(Bug 113914148): Consider removing this.
int getInputMethodWindowVisibleHeight(in IInputMethodClient client);

View File

@@ -75,6 +75,7 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
@@ -4214,6 +4215,46 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
}
}
@Override
public void setExplicitlyEnabledInputMethodSubtypes(String imeId,
@NonNull int[] subtypeHashCodes, @UserIdInt int userId) {
if (UserHandle.getCallingUserId() != userId) {
mContext.enforceCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
final int callingUid = Binder.getCallingUid();
final ComponentName imeComponentName =
imeId != null ? ComponentName.unflattenFromString(imeId) : null;
if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager,
callingUid, imeComponentName.getPackageName())) {
throw new SecurityException("Calling UID=" + callingUid + " does not belong to imeId="
+ imeId);
}
Objects.requireNonNull(subtypeHashCodes, "subtypeHashCodes must not be null");
final long ident = Binder.clearCallingIdentity();
try {
synchronized (ImfLock.class) {
final boolean currentUser = (mSettings.getCurrentUserId() == userId);
final InputMethodSettings settings = currentUser
? mSettings
: new InputMethodSettings(mContext, queryMethodMapForUser(userId), userId,
!mUserManagerInternal.isUserUnlocked(userId));
if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) {
return;
}
if (currentUser) {
// To avoid unnecessary "updateInputMethodsFromSettingsLocked" from happening.
if (mSettingsObserver != null) {
mSettingsObserver.mLastEnabled = settings.getEnabledInputMethodsStr();
}
updateInputMethodsFromSettingsLocked(false /* enabledChanged */);
}
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
/**
* This is kept due to {@code @UnsupportedAppUsage} in
* {@link InputMethodManager#getInputMethodWindowVisibleHeight()} and a dependency in

View File

@@ -35,6 +35,7 @@ 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;
@@ -42,6 +43,7 @@ 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.internal.util.ArrayUtils;
import com.android.server.LocalServices;
@@ -901,6 +903,72 @@ final class InputMethodUtils {
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 + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));

View File

@@ -35,11 +35,14 @@ import android.content.res.Resources;
import android.os.Build;
import android.os.LocaleList;
import android.os.Parcel;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.IntArray;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -1210,4 +1213,79 @@ public class InputMethodUtilsTest {
Build.VERSION_CODES.P,
StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR));
}
private static IntArray createSubtypeHashCodeArrayFromStr(String subtypeHashCodesStr) {
final IntArray subtypes = new IntArray();
final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
new TextUtils.SimpleStringSplitter(';');
if (TextUtils.isEmpty(subtypeHashCodesStr)) {
return subtypes;
}
imeSubtypeSplitter.setString(subtypeHashCodesStr);
while (imeSubtypeSplitter.hasNext()) {
subtypes.add(Integer.parseInt(imeSubtypeSplitter.next()));
}
return subtypes;
}
private static void verifyUpdateEnabledImeString(@NonNull String expectedEnabledImeStr,
@NonNull String initialEnabledImeStr, @NonNull String imeId,
@NonNull String enabledSubtypeHashCodesStr) {
assertEquals(expectedEnabledImeStr,
InputMethodUtils.InputMethodSettings.updateEnabledImeString(initialEnabledImeStr,
imeId, createSubtypeHashCodeArrayFromStr(enabledSubtypeHashCodesStr)));
}
@Test
public void updateEnabledImeStringTest() {
// No change cases
verifyUpdateEnabledImeString(
"com.android/.ime1",
"com.android/.ime1", "com.android/.ime1", "");
verifyUpdateEnabledImeString(
"com.android/.ime1",
"com.android/.ime1", "com.android/.ime2", "");
// To enable subtypes
verifyUpdateEnabledImeString(
"com.android/.ime1",
"com.android/.ime1", "com.android/.ime2", "");
verifyUpdateEnabledImeString(
"com.android/.ime1;1",
"com.android/.ime1", "com.android/.ime1", "1");
verifyUpdateEnabledImeString(
"com.android/.ime1;1;2;3",
"com.android/.ime1", "com.android/.ime1", "1;2;3");
verifyUpdateEnabledImeString(
"com.android/.ime1;1;2;3:com.android/.ime2",
"com.android/.ime1:com.android/.ime2", "com.android/.ime1", "1;2;3");
verifyUpdateEnabledImeString(
"com.android/.ime0:com.android/.ime1;1;2;3",
"com.android/.ime0:com.android/.ime1", "com.android/.ime1", "1;2;3");
verifyUpdateEnabledImeString(
"com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2",
"com.android/.ime0:com.android/.ime1:com.android/.ime2", "com.android/.ime1",
"1;2;3");
// To reset enabled subtypes
verifyUpdateEnabledImeString(
"com.android/.ime1",
"com.android/.ime1;1", "com.android/.ime1", "");
verifyUpdateEnabledImeString(
"com.android/.ime1",
"com.android/.ime1;1;2;3", "com.android/.ime1", "");
verifyUpdateEnabledImeString(
"com.android/.ime1:com.android/.ime2",
"com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1", "");
verifyUpdateEnabledImeString(
"com.android/.ime0:com.android/.ime1",
"com.android/.ime0:com.android/.ime1;1;2;3", "com.android/.ime1", "");
verifyUpdateEnabledImeString(
"com.android/.ime0:com.android/.ime1:com.android/.ime2",
"com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1",
"");
}
}