Add new setInputMethodEnabled API
Make accessibility services able to enable/disable a specified
IME in the same package for the current user.
Bug: 195476910
Fix: 191386474
Test: atest AccessibilitySoftKeyboardTest. Also tested with
modified talkback (use the swith input method API for enable
ime), and self created empty a11y service with an empty ime.
Mananged device and profile which set limit on user 0 are also
tested.
Change-Id: I4187468076705ac597d680f2f5dc32d7b166da1f
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index 1698e9a..bf8a9af 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -16,7 +16,15 @@
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",
+ ],
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java
index 0ab0c89..e251bcc 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java
@@ -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.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;
/**
@@ -364,6 +373,115 @@
}
/**
+ * 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.
*
* @param userId The userId to check
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index bcb3413..8f7260f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -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;
@@ -310,6 +314,41 @@
}
@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()) {
logTraceSvcConn("isAccessibilityButtonAvailable", "");
diff --git a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
index 7ee0690..6cc2214 100644
--- a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
@@ -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;
@@ -333,6 +334,11 @@
}
@Override
+ public int setInputMethodEnabled(String imeId, boolean enabled) {
+ return AccessibilityService.SoftKeyboardController.ENABLE_IME_FAIL_UNKNOWN;
+ }
+
+ @Override
public boolean isAccessibilityButtonAvailable() {
return false;
}