Merge "Add new setInputMethodEnabled API"

This commit is contained in:
Yinglei Wang
2021-12-15 19:23:34 +00:00
committed by Android (Google) Code Review
12 changed files with 325 additions and 33 deletions

View File

@@ -3148,8 +3148,12 @@ package android.accessibilityservice {
method public void addOnShowModeChangedListener(@NonNull android.accessibilityservice.AccessibilityService.SoftKeyboardController.OnShowModeChangedListener, @Nullable android.os.Handler);
method public int getShowMode();
method public boolean removeOnShowModeChangedListener(@NonNull android.accessibilityservice.AccessibilityService.SoftKeyboardController.OnShowModeChangedListener);
method @CheckResult public int setInputMethodEnabled(@NonNull String, boolean) throws java.lang.SecurityException;
method public boolean setShowMode(int);
method public boolean switchToInputMethod(@NonNull String);
field public static final int ENABLE_IME_FAIL_BY_ADMIN = 1; // 0x1
field public static final int ENABLE_IME_FAIL_UNKNOWN = 2; // 0x2
field public static final int ENABLE_IME_SUCCESS = 0; // 0x0
}
public static interface AccessibilityService.SoftKeyboardController.OnShowModeChangedListener {

View File

@@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
import android.accessibilityservice.GestureDescription.MotionEventGenerator;
import android.annotation.CallbackExecutor;
import android.annotation.CheckResult;
import android.annotation.ColorInt;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -1733,6 +1734,29 @@ public abstract class AccessibilityService extends Service {
private ArrayMap<OnShowModeChangedListener, Handler> mListeners;
private final Object mLock;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
ENABLE_IME_SUCCESS,
ENABLE_IME_FAIL_BY_ADMIN,
ENABLE_IME_FAIL_UNKNOWN
})
public @interface EnableImeResult {}
/**
* Return value for {@link #setInputMethodEnabled(String, boolean)}. The action succeeded.
*/
public static final int ENABLE_IME_SUCCESS = 0;
/**
* Return value for {@link #setInputMethodEnabled(String, boolean)}. The action failed
* because the InputMethod is not permitted by device policy manager.
*/
public static final int ENABLE_IME_FAIL_BY_ADMIN = 1;
/**
* Return value for {@link #setInputMethodEnabled(String, boolean)}. The action failed
* and the reason is unknown.
*/
public static final int ENABLE_IME_FAIL_UNKNOWN = 2;
SoftKeyboardController(@NonNull AccessibilityService service, @NonNull Object lock) {
mService = service;
mLock = lock;
@@ -1961,6 +1985,39 @@ public abstract class AccessibilityService extends Service {
}
return false;
}
/**
* Enable or disable the specified IME for the user for whom the service is activated. The
* IME needs to be in the same package as the service and needs to be allowed by device
* policy, if there is one. The change will persist until the specified IME is next
* explicitly enabled or disabled by whatever means, such as user choice, and may persist
* beyond the life cycle of the requesting service.
*
* @param imeId The ID of the input method to enable or disable. This IME must be installed.
* @param enabled {@code true} if the input method associated with {@code imeId} should be
* enabled.
* @return status code for the result of enabling/disabling the input method associated
* with {@code imeId}.
* @throws SecurityException if the input method is not in the same package as the service.
*
* @see android.view.inputmethod.InputMethodInfo#getId()
*/
@CheckResult
@EnableImeResult
public int setInputMethodEnabled(@NonNull String imeId, boolean enabled)
throws SecurityException {
final IAccessibilityServiceConnection connection =
AccessibilityInteractionClient.getInstance(mService).getConnection(
mService.mConnectionId);
if (connection != null) {
try {
return connection.setInputMethodEnabled(imeId, enabled);
} catch (RemoteException re) {
throw new RuntimeException(re);
}
}
return ENABLE_IME_FAIL_UNKNOWN;
}
}
/**

View File

@@ -102,6 +102,8 @@ interface IAccessibilityServiceConnection {
boolean switchToInputMethod(String imeId);
int setInputMethodEnabled(String imeId, boolean enabled);
boolean isAccessibilityButtonAvailable();
void sendGesture(int sequence, in ParceledListSlice gestureSteps);

View File

@@ -16,6 +16,7 @@
package android.view.accessibility;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceConnection;
import android.accessibilityservice.MagnificationConfig;
@@ -144,6 +145,10 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon
return false;
}
public int setInputMethodEnabled(String imeId, boolean enabled) {
return AccessibilityService.SoftKeyboardController.ENABLE_IME_FAIL_UNKNOWN;
}
public boolean isAccessibilityButtonAvailable() {
return false;
}

View File

@@ -7,6 +7,12 @@ package {
default_applicable_licenses: ["frameworks_base_license"],
}
filegroup {
name: "SettingsLibRestrictedLockUtilsSrc",
srcs: ["src/**/*.java"],
visibility: ["//frameworks/base/services/accessibility"],
}
android_library {
name: "SettingsLibRestrictedLockUtils",

View File

@@ -16,7 +16,15 @@ filegroup {
java_library_static {
name: "services.accessibility",
defaults: ["platform_service_defaults"],
srcs: [":services.accessibility-sources"],
libs: ["services.core"],
defaults: [
"platform_service_defaults",
],
srcs: [
":services.accessibility-sources",
"//frameworks/base/packages/SettingsLib/RestrictedLockUtils:SettingsLibRestrictedLockUtilsSrc",
],
libs: [
"services.core",
"androidx.annotation_annotation",
],
}

View File

@@ -16,13 +16,18 @@
package com.android.server.accessibility;
import static android.accessibilityservice.AccessibilityService.SoftKeyboardController.ENABLE_IME_FAIL_BY_ADMIN;
import static android.accessibilityservice.AccessibilityService.SoftKeyboardController.ENABLE_IME_SUCCESS;
import static android.content.pm.PackageManagerInternal.PACKAGE_INSTALLER;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManager;
import android.appwidget.AppWidgetManagerInternal;
import android.content.ComponentName;
import android.content.Context;
@@ -41,13 +46,17 @@ import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Slog;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.InputMethodInfo;
import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.settingslib.RestrictedLockUtils;
import libcore.util.EmptyArray;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
@@ -363,6 +372,115 @@ public class AccessibilitySecurityPolicy {
& AccessibilityServiceInfo.CAPABILITY_CAN_TAKE_SCREENSHOT) != 0;
}
/**
* Check whether the input method can be enabled or disabled by the accessibility service.
*
* @param imeId The id of the input method.
* @param service The accessibility service connection.
* @return Whether the input method can be enabled/disabled or the reason why it can't be
* enabled/disabled.
* @throws SecurityException if the input method is not in the same package as the service.
*/
@AccessibilityService.SoftKeyboardController.EnableImeResult
int canEnableDisableInputMethod(String imeId, AbstractAccessibilityServiceConnection service)
throws SecurityException {
final String servicePackageName = service.getComponentName().getPackageName();
final int callingUserId = UserHandle.getCallingUserId();
InputMethodInfo inputMethodInfo = null;
List<InputMethodInfo> inputMethodInfoList =
InputMethodManagerInternal.get().getInputMethodListAsUser(callingUserId);
if (inputMethodInfoList != null) {
for (InputMethodInfo info : inputMethodInfoList) {
if (info.getId().equals(imeId)) {
inputMethodInfo = info;
break;
}
}
}
if (inputMethodInfo == null
|| !inputMethodInfo.getPackageName().equals(servicePackageName)) {
throw new SecurityException("The input method is in a different package with the "
+ "accessibility service");
}
// TODO(b/207697949, b/208872785): Add cts test for managed device.
// Use RestrictedLockUtilsInternal in AccessibilitySecurityPolicy
if (checkIfInputMethodDisallowed(
mContext, inputMethodInfo.getPackageName(), callingUserId) != null) {
return ENABLE_IME_FAIL_BY_ADMIN;
}
return ENABLE_IME_SUCCESS;
}
/**
* @return the UserHandle for a userId. Return null for USER_NULL
*/
private static UserHandle getUserHandleOf(@UserIdInt int userId) {
if (userId == UserHandle.USER_NULL) {
return null;
} else {
return UserHandle.of(userId);
}
}
private static int getManagedProfileId(Context context, int userId) {
UserManager um = context.getSystemService(UserManager.class);
List<UserInfo> userProfiles = um.getProfiles(userId);
for (UserInfo uInfo : userProfiles) {
if (uInfo.id == userId) {
continue;
}
if (uInfo.isManagedProfile()) {
return uInfo.id;
}
}
return UserHandle.USER_NULL;
}
private static RestrictedLockUtils.EnforcedAdmin checkIfInputMethodDisallowed(Context context,
String packageName, int userId) {
DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
if (dpm == null) {
return null;
}
RestrictedLockUtils.EnforcedAdmin admin =
RestrictedLockUtils.getProfileOrDeviceOwner(context, getUserHandleOf(userId));
boolean permitted = true;
if (admin != null) {
permitted = dpm.isInputMethodPermittedByAdmin(admin.component,
packageName, userId);
}
boolean permittedByParentAdmin = true;
RestrictedLockUtils.EnforcedAdmin profileAdmin = null;
int managedProfileId = getManagedProfileId(context, userId);
if (managedProfileId != UserHandle.USER_NULL) {
profileAdmin = RestrictedLockUtils.getProfileOrDeviceOwner(
context, getUserHandleOf(managedProfileId));
// If the device is an organization-owned device with a managed profile, the
// managedProfileId will be used instead of the affected userId. This is because
// isInputMethodPermittedByAdmin is called on the parent DPM instance, which will
// return results affecting the personal profile.
if (profileAdmin != null && dpm.isOrganizationOwnedDeviceWithManagedProfile()) {
DevicePolicyManager parentDpm = dpm.getParentProfileInstance(
UserManager.get(context).getUserInfo(managedProfileId));
permittedByParentAdmin = parentDpm.isInputMethodPermittedByAdmin(
profileAdmin.component, packageName, managedProfileId);
}
}
if (!permitted && !permittedByParentAdmin) {
return RestrictedLockUtils.EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
} else if (!permitted) {
return admin;
} else if (!permittedByParentAdmin) {
return profileAdmin;
}
return null;
}
/**
* Returns the parent userId of the profile according to the specified userId.
*

View File

@@ -16,9 +16,13 @@
package com.android.server.accessibility;
import static android.accessibilityservice.AccessibilityService.SoftKeyboardController.ENABLE_IME_FAIL_UNKNOWN;
import static android.accessibilityservice.AccessibilityService.SoftKeyboardController.ENABLE_IME_SUCCESS;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityTrace;
import android.accessibilityservice.IAccessibilityServiceClient;
@@ -309,6 +313,41 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect
return result;
}
@Override
@AccessibilityService.SoftKeyboardController.EnableImeResult
public int setInputMethodEnabled(String imeId, boolean enabled) throws SecurityException {
if (svcConnTracingEnabled()) {
logTraceSvcConn("switchToInputMethod", "imeId=" + imeId);
}
synchronized (mLock) {
if (!hasRightsToCurrentUserLocked()) {
return ENABLE_IME_FAIL_UNKNOWN;
}
}
final int callingUserId = UserHandle.getCallingUserId();
final InputMethodManagerInternal inputMethodManagerInternal =
InputMethodManagerInternal.get();
final @AccessibilityService.SoftKeyboardController.EnableImeResult int checkResult;
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
checkResult = mSecurityPolicy.canEnableDisableInputMethod(imeId, this);
}
if (checkResult != ENABLE_IME_SUCCESS) {
return checkResult;
}
if (inputMethodManagerInternal.setInputMethodEnabled(imeId,
enabled, callingUserId)) {
return ENABLE_IME_SUCCESS;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return ENABLE_IME_FAIL_UNKNOWN;
}
@Override
public boolean isAccessibilityButtonAvailable() {
if (svcConnTracingEnabled()) {

View File

@@ -16,6 +16,7 @@
package com.android.server.accessibility;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityTrace;
import android.accessibilityservice.IAccessibilityServiceClient;
@@ -332,6 +333,11 @@ class UiAutomationManager {
return false;
}
@Override
public int setInputMethodEnabled(String imeId, boolean enabled) {
return AccessibilityService.SoftKeyboardController.ENABLE_IME_FAIL_UNKNOWN;
}
@Override
public boolean isAccessibilityButtonAvailable() {
return false;

View File

@@ -94,6 +94,21 @@ public abstract class InputMethodManagerInternal {
*/
public abstract boolean switchToInputMethod(String imeId, @UserIdInt int userId);
/**
* Force enable or disable the input method associated with {@code imeId} for given user. If
* the input method associated with {@code imeId} is not installed, do nothing.
*
* @param imeId The input method ID to be enabled or disabled.
* @param enabled {@code true} if the input method associated with {@code imeId} should be
* enabled.
* @param userId The user ID to be queried.
* @return {@code true} if the input method associated with {@code imeId} was successfully
* enabled or disabled, {@code false} if the input method specified is not installed
* or was unable to be enabled/disabled for some other reason.
*/
public abstract boolean setInputMethodEnabled(String imeId, boolean enabled,
@UserIdInt int userId);
/**
* Registers a new {@link InputMethodListListener}.
*/
@@ -167,6 +182,11 @@ public abstract class InputMethodManagerInternal {
return false;
}
@Override
public boolean setInputMethodEnabled(String imeId, boolean enabled, int userId) {
return false;
}
@Override
public void registerInputMethodListListener(InputMethodListListener listener) {
}

View File

@@ -1934,13 +1934,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
if (userId == mSettings.getCurrentUserId()) {
return mSettings.getEnabledInputMethodListLocked();
}
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
methodList);
final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
final InputMethodSettings settings = new InputMethodSettings(mContext.getResources(),
mContext.getContentResolver(), methodMap, userId, true);
return settings.getEnabledInputMethodListLocked();
@@ -2105,13 +2099,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return mSettings.getEnabledInputMethodSubtypeListLocked(
mContext, imi, allowsImplicitlySelectedSubtypes);
}
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
methodList);
final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
final InputMethodInfo imi = methodMap.get(imiId);
if (imi == null) {
return Collections.emptyList();
@@ -4782,31 +4770,36 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
}
private ArrayMap<String, InputMethodInfo> queryMethodMapForUser(@UserIdInt int userId) {
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
methodMap, methodList);
return methodMap;
}
private boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
synchronized (mMethodMap) {
if (userId == mSettings.getCurrentUserId()) {
if (!mMethodMap.containsKey(imeId)
|| !mSettings.getEnabledInputMethodListLocked()
.contains(mMethodMap.get(imeId))) {
return false; // IME is not is found or not enabled.
return false; // IME is not found or not enabled.
}
setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID);
return true;
}
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
methodMap, methodList);
final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
final InputMethodSettings settings = new InputMethodSettings(
mContext.getResources(), mContext.getContentResolver(), methodMap,
userId, false);
if (!methodMap.containsKey(imeId)
|| !settings.getEnabledInputMethodListLocked()
.contains(methodMap.get(imeId))) {
return false; // IME is not is found or not enabled.
return false; // IME is not found or not enabled.
}
settings.putSelectedInputMethod(imeId);
settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
@@ -4814,6 +4807,35 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
}
private boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) {
synchronized (mMethodMap) {
if (userId == mSettings.getCurrentUserId()) {
if (!mMethodMap.containsKey(imeId)) {
return false; // IME is not found.
}
setInputMethodEnabledLocked(imeId, enabled);
return true;
}
final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
final InputMethodSettings settings = new InputMethodSettings(
mContext.getResources(), mContext.getContentResolver(), methodMap,
userId, false);
if (!methodMap.containsKey(imeId)) {
return false; // IME is not found.
}
if (enabled) {
if (!settings.getEnabledInputMethodListLocked().contains(methodMap.get(imeId))) {
settings.appendAndPutEnabledInputMethodLocked(imeId, false);
}
} else {
settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
new StringBuilder(),
settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId);
}
return true;
}
}
private boolean transferTouchFocusToImeWindow(@NonNull IBinder sourceInputToken,
int displayId) {
//TODO(b/150843766): Check if Input Token is valid.
@@ -4885,6 +4907,11 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return mService.switchToInputMethod(imeId, userId);
}
@Override
public boolean setInputMethodEnabled(String imeId, boolean enabled, int userId) {
return mService.setInputMethodEnabled(imeId, enabled, userId);
}
@Override
public void registerInputMethodListListener(InputMethodListListener listener) {
mService.mInputMethodListListeners.addIfAbsent(listener);
@@ -5482,13 +5509,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled);
}
} else {
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
methodMap, methodList);
final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
final InputMethodSettings settings = new InputMethodSettings(mContext.getResources(),
mContext.getContentResolver(), methodMap, userId, false);
if (enabled) {

View File

@@ -66,6 +66,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityTrace;
import android.accessibilityservice.IAccessibilityServiceClient;
@@ -841,6 +842,11 @@ public class AbstractAccessibilityServiceConnectionTest {
return false;
}
@Override
public int setInputMethodEnabled(String imeId, boolean enabled) throws RemoteException {
return AccessibilityService.SoftKeyboardController.ENABLE_IME_FAIL_UNKNOWN;
}
@Override
public boolean isAccessibilityButtonAvailable() throws RemoteException {
return false;