diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 9504852f6e98b..46b08cf3f13f6 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -2210,6 +2210,58 @@ public final class AccessibilityManager { } } + /** + * Determines if the accessibility target is allowed. + * + * @param packageName The name of the application attempting to perform the operation. + * @param uid The user id of the application attempting to perform the operation. + * @param userId The id of the user for whom to perform the operation. + * @return {@code true} the accessibility target is allowed. + * @hide + */ + public boolean isAccessibilityTargetAllowed(String packageName, int uid, int userId) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return false; + } + } + + try { + return service.isAccessibilityTargetAllowed(packageName, uid, userId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while check accessibility target status", re); + return false; + } + } + + /** + * Sends restricted dialog intent if the accessibility target is disallowed. + * + * @param packageName The name of the application attempting to perform the operation. + * @param uid The user id of the application attempting to perform the operation. + * @param userId The id of the user for whom to perform the operation. + * @return {@code true} if the restricted dialog is shown. + * @hide + */ + public boolean sendRestrictedDialogIntent(String packageName, int uid, int userId) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return false; + } + } + + try { + return service.sendRestrictedDialogIntent(packageName, uid, userId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while show restricted dialog", re); + return false; + } + } + private IAccessibilityManager getServiceLocked() { if (mService == null) { tryConnectToServiceLocked(null); diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 1302421d0ce2e..d95f4ffc0e9fe 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -126,6 +126,9 @@ interface IAccessibilityManager { boolean stopFlashNotificationSequence(String opPkg); boolean startFlashNotificationEvent(String opPkg, int reason, String reasonPkg); + boolean isAccessibilityTargetAllowed(String packageName, int uid, int userId); + boolean sendRestrictedDialogIntent(String packageName, int uid, int userId); + parcelable WindowTransformationSpec { float[] transformationMatrix; MagnificationSpec magnificationSpec; diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java index 4c7d93b7446a8..063154d9a6d6a 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityActivityTarget.java @@ -26,6 +26,7 @@ import android.content.Context; import android.view.accessibility.AccessibilityManager.ShortcutType; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; +import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode; /** * Base class for creating accessibility activity target. @@ -40,8 +41,25 @@ class AccessibilityActivityTarget extends AccessibilityTarget { isShortcutContained(context, shortcutType, shortcutInfo.getComponentName().flattenToString()), shortcutInfo.getComponentName().flattenToString(), + shortcutInfo.getActivityInfo().applicationInfo.uid, shortcutInfo.getActivityInfo().loadLabel(context.getPackageManager()), shortcutInfo.getActivityInfo().loadIcon(context.getPackageManager()), convertToKey(convertToUserType(shortcutType))); } + + @Override + public void updateActionItem(@NonNull TargetAdapter.ViewHolder holder, + @ShortcutMenuMode int shortcutMenuMode) { + super.updateActionItem(holder, shortcutMenuMode); + + final boolean isAllowed = AccessibilityTargetHelper.isAccessibilityTargetAllowed( + getContext(), getComponentName().getPackageName(), getUid()); + final boolean isEditMenuMode = + shortcutMenuMode == ShortcutMenuMode.EDIT; + final boolean enabled = isAllowed || (isEditMenuMode && isShortcutEnabled()); + holder.mCheckBoxView.setEnabled(enabled); + holder.mIconView.setEnabled(enabled); + holder.mLabelView.setEnabled(enabled); + holder.mStatusView.setEnabled(enabled); + } } diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java index e64f78a3f2070..64974090938f0 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java @@ -26,6 +26,7 @@ import android.content.Context; import android.view.accessibility.AccessibilityManager.ShortcutType; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; +import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode; /** * Base class for creating accessibility service target with various fragment types related to @@ -42,8 +43,25 @@ class AccessibilityServiceTarget extends AccessibilityTarget { isShortcutContained(context, shortcutType, serviceInfo.getComponentName().flattenToString()), serviceInfo.getComponentName().flattenToString(), + serviceInfo.getResolveInfo().serviceInfo.applicationInfo.uid, serviceInfo.getResolveInfo().loadLabel(context.getPackageManager()), serviceInfo.getResolveInfo().loadIcon(context.getPackageManager()), convertToKey(convertToUserType(shortcutType))); } + + @Override + public void updateActionItem(@NonNull TargetAdapter.ViewHolder holder, + @ShortcutMenuMode int shortcutMenuMode) { + super.updateActionItem(holder, shortcutMenuMode); + + final boolean isAllowed = AccessibilityTargetHelper.isAccessibilityTargetAllowed( + getContext(), getComponentName().getPackageName(), getUid()); + final boolean isEditMenuMode = + shortcutMenuMode == ShortcutMenuMode.EDIT; + final boolean enabled = isAllowed || (isEditMenuMode && isShortcutEnabled()); + holder.mCheckBoxView.setEnabled(enabled); + holder.mIconView.setEnabled(enabled); + holder.mLabelView.setEnabled(enabled); + holder.mStatusView.setEnabled(enabled); + } } diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java index 50afb3ee0a03c..5dd558a5f850e 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java @@ -95,6 +95,13 @@ public class AccessibilityShortcutChooserActivity extends Activity { private void onTargetSelected(AdapterView parent, View view, int position, long id) { final AccessibilityTarget target = mTargets.get(position); + if (target instanceof AccessibilityServiceTarget + || target instanceof AccessibilityActivityTarget) { + if (sendRestrictedDialogIntentIfNeeded(target)) { + return; + } + } + target.onSelected(); mMenuDialog.dismiss(); } @@ -102,15 +109,41 @@ public class AccessibilityShortcutChooserActivity extends Activity { private void onTargetChecked(AdapterView parent, View view, int position, long id) { final AccessibilityTarget target = mTargets.get(position); - if ((target instanceof AccessibilityServiceTarget) && !target.isShortcutEnabled()) { - showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, mTargetAdapter); - return; + if (!target.isShortcutEnabled()) { + if (target instanceof AccessibilityServiceTarget + || target instanceof AccessibilityActivityTarget) { + if (sendRestrictedDialogIntentIfNeeded(target)) { + return; + } + } + + if (target instanceof AccessibilityServiceTarget) { + showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, + mTargetAdapter); + return; + } } target.onCheckedChanged(!target.isShortcutEnabled()); mTargetAdapter.notifyDataSetChanged(); } + /** + * Sends restricted dialog intent if the accessibility target is disallowed. + * + * @return true if sends restricted dialog intent, otherwise false. + */ + private boolean sendRestrictedDialogIntentIfNeeded(AccessibilityTarget target) { + if (AccessibilityTargetHelper.isAccessibilityTargetAllowed(this, + target.getComponentName().getPackageName(), target.getUid())) { + return false; + } + + AccessibilityTargetHelper.sendRestrictedDialogIntent(this, + target.getComponentName().getPackageName(), target.getUid()); + return true; + } + private void showPermissionDialogIfNeeded(Context context, AccessibilityServiceTarget serviceTarget, ShortcutTargetAdapter targetAdapter) { if (mPermissionDialog != null) { diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java index b8446da3c5fcc..652cb52334610 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java @@ -25,6 +25,7 @@ import static com.android.internal.accessibility.util.ShortcutUtils.optOutValueF import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.ComponentName; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.View; @@ -37,8 +38,11 @@ import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; import com.android.internal.annotations.VisibleForTesting; /** - * Abstract base class for creating various target related to accessibility service, - * accessibility activity, and allowlisting feature. + * Abstract base class for creating various target related to accessibility service, accessibility + * activity, and allowlisting features. + * + *

Disables accessibility features that are not permitted in adding a restricted padlock icon + * and showing admin support message dialog. */ public abstract class AccessibilityTarget implements TargetOperations, OnTargetSelectedListener, OnTargetCheckedChangeListener { @@ -49,6 +53,8 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS private int mFragmentType; private boolean mShortcutEnabled; private String mId; + private int mUid; + private ComponentName mComponentName; private CharSequence mLabel; private Drawable mIcon; private String mKey; @@ -57,12 +63,14 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS @VisibleForTesting public AccessibilityTarget(Context context, @ShortcutType int shortcutType, @AccessibilityFragmentType int fragmentType, boolean isShortcutSwitched, String id, - CharSequence label, Drawable icon, String key) { + int uid, CharSequence label, Drawable icon, String key) { mContext = context; mShortcutType = shortcutType; mFragmentType = fragmentType; mShortcutEnabled = isShortcutSwitched; mId = id; + mUid = uid; + mComponentName = ComponentName.unflattenFromString(id); mLabel = label; mIcon = icon; mKey = key; @@ -71,9 +79,14 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS @Override public void updateActionItem(@NonNull ViewHolder holder, @ShortcutConstants.ShortcutMenuMode int shortcutMenuMode) { + // Resetting the enable state of the item to avoid the previous wrong state of RecyclerView. + holder.mCheckBoxView.setEnabled(true); + holder.mIconView.setEnabled(true); + holder.mLabelView.setEnabled(true); + holder.mStatusView.setEnabled(true); + final boolean isEditMenuMode = shortcutMenuMode == ShortcutConstants.ShortcutMenuMode.EDIT; - holder.mCheckBoxView.setChecked(isEditMenuMode && isShortcutEnabled()); holder.mCheckBoxView.setVisibility(isEditMenuMode ? View.VISIBLE : View.GONE); holder.mIconView.setImageDrawable(getIcon()); @@ -145,6 +158,14 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS return mId; } + public int getUid() { + return mUid; + } + + public ComponentName getComponentName() { + return mComponentName; + } + public CharSequence getLabel() { return mLabel; } diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java index a47a97c02bfbc..0f85075a0d6c9 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -35,6 +35,7 @@ import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.os.Build; +import android.os.UserHandle; import android.provider.Settings; import android.text.BidiFormatter; import android.view.LayoutInflater; @@ -202,12 +203,14 @@ public final class AccessibilityTargetHelper { private static List getAllowListingFeatureTargets(Context context, @ShortcutType int shortcutType) { final List targets = new ArrayList<>(); + final int uid = context.getApplicationInfo().uid; final InvisibleToggleAllowListingFeatureTarget magnification = new InvisibleToggleAllowListingFeatureTarget(context, shortcutType, isShortcutContained(context, shortcutType, MAGNIFICATION_CONTROLLER_NAME), MAGNIFICATION_CONTROLLER_NAME, + uid, context.getString(R.string.accessibility_magnification_chooser_text), context.getDrawable(R.drawable.ic_accessibility_magnification), Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED); @@ -219,6 +222,7 @@ public final class AccessibilityTargetHelper { isShortcutContained(context, shortcutType, DALTONIZER_COMPONENT_NAME.flattenToString()), DALTONIZER_COMPONENT_NAME.flattenToString(), + uid, context.getString(R.string.color_correction_feature_name), context.getDrawable(R.drawable.ic_accessibility_color_correction), Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED); @@ -230,6 +234,7 @@ public final class AccessibilityTargetHelper { isShortcutContained(context, shortcutType, COLOR_INVERSION_COMPONENT_NAME.flattenToString()), COLOR_INVERSION_COMPONENT_NAME.flattenToString(), + uid, context.getString(R.string.color_inversion_feature_name), context.getDrawable(R.drawable.ic_accessibility_color_inversion), Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); @@ -242,6 +247,7 @@ public final class AccessibilityTargetHelper { isShortcutContained(context, shortcutType, ONE_HANDED_COMPONENT_NAME.flattenToString()), ONE_HANDED_COMPONENT_NAME.flattenToString(), + uid, context.getString(R.string.one_handed_mode_feature_name), context.getDrawable(R.drawable.ic_accessibility_one_handed), Settings.Secure.ONE_HANDED_MODE_ACTIVATED); @@ -254,6 +260,7 @@ public final class AccessibilityTargetHelper { isShortcutContained(context, shortcutType, REDUCE_BRIGHT_COLORS_COMPONENT_NAME.flattenToString()), REDUCE_BRIGHT_COLORS_COMPONENT_NAME.flattenToString(), + uid, context.getString(R.string.reduce_bright_colors_feature_name), context.getDrawable(R.drawable.ic_accessibility_reduce_bright_colors), Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED); @@ -265,6 +272,7 @@ public final class AccessibilityTargetHelper { isShortcutContained(context, shortcutType, ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()), ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString(), + uid, context.getString(R.string.hearing_aids_feature_name), context.getDrawable(R.drawable.ic_accessibility_hearing_aid), /* key= */ null); @@ -327,4 +335,21 @@ public final class AccessibilityTargetHelper { final Locale locale = context.getResources().getConfiguration().getLocales().get(0); return BidiFormatter.getInstance(locale).unicodeWrap(label); } + + /** + * Determines if the{@link AccessibilityTarget} is allowed. + */ + public static boolean isAccessibilityTargetAllowed(Context context, String packageName, + int uid) { + final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); + return am.isAccessibilityTargetAllowed(packageName, uid, UserHandle.myUserId()); + } + + /** + * Sends restricted dialog intent if the accessibility target is disallowed. + */ + public static boolean sendRestrictedDialogIntent(Context context, String packageName, int uid) { + final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); + return am.sendRestrictedDialogIntent(packageName, uid, UserHandle.myUserId()); + } } diff --git a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java index e78036d9f1e9b..c22f17dfa9671 100644 --- a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAllowListingFeatureTarget.java @@ -29,8 +29,9 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility class InvisibleToggleAllowListingFeatureTarget extends AccessibilityTarget { InvisibleToggleAllowListingFeatureTarget(Context context, @ShortcutType int shortcutType, - boolean isShortcutSwitched, String id, CharSequence label, Drawable icon, String key) { - super(context, shortcutType, AccessibilityFragmentType.INVISIBLE_TOGGLE, - isShortcutSwitched, id, label, icon, key); + boolean isShortcutSwitched, String id, int uid, CharSequence label, Drawable icon, + String key) { + super(context, shortcutType, AccessibilityFragmentType.INVISIBLE_TOGGLE, isShortcutSwitched, + id, uid, label, icon, key); } } diff --git a/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java index 41a0ba2b693aa..a4ffef6bfbc2f 100644 --- a/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java @@ -29,12 +29,22 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode; import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Extension for {@link AccessibilityServiceTarget} with {@link AccessibilityFragmentType#TOGGLE} * type. */ class ToggleAccessibilityServiceTarget extends AccessibilityServiceTarget { + /** Float enum for view alpha setting. */ + @Retention(RetentionPolicy.SOURCE) + @interface StatusViewAlphaScale { + float OPAQUE = 1.0f; + float DISABLED = 0.5f; + } + ToggleAccessibilityServiceTarget(Context context, @ShortcutType int shortcutType, @NonNull AccessibilityServiceInfo serviceInfo) { super(context, @@ -53,9 +63,13 @@ class ToggleAccessibilityServiceTarget extends AccessibilityServiceTarget { @ShortcutMenuMode int shortcutMenuMode) { super.updateActionItem(holder, shortcutMenuMode); + final boolean isAllowed = AccessibilityTargetHelper.isAccessibilityTargetAllowed( + getContext(), getComponentName().getPackageName(), getUid()); final boolean isEditMenuMode = shortcutMenuMode == ShortcutMenuMode.EDIT; holder.mStatusView.setVisibility(isEditMenuMode ? View.GONE : View.VISIBLE); holder.mStatusView.setText(getStateDescription()); + holder.mStatusView.setAlpha(isAllowed + ? StatusViewAlphaScale.OPAQUE : StatusViewAlphaScale.DISABLED); } } diff --git a/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java b/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java index d2124a00596f3..11e668f517744 100644 --- a/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java @@ -35,9 +35,10 @@ import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; class ToggleAllowListingFeatureTarget extends AccessibilityTarget { ToggleAllowListingFeatureTarget(Context context, @ShortcutType int shortcutType, - boolean isShortcutSwitched, String id, CharSequence label, Drawable icon, String key) { - super(context, shortcutType, AccessibilityFragmentType.TOGGLE, - isShortcutSwitched, id, label, icon, key); + boolean isShortcutSwitched, String id, int uid, CharSequence label, Drawable icon, + String key) { + super(context, shortcutType, AccessibilityFragmentType.TOGGLE, isShortcutSwitched, id, + uid, label, icon, key); final int statusResId = isFeatureEnabled() ? R.string.accessibility_shortcut_menu_item_status_on diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java index 973b904c9344f..03d366e6e5522 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java @@ -33,6 +33,9 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.endsWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.accessibilityservice.AccessibilityServiceInfo; @@ -54,6 +57,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -71,6 +75,7 @@ public class AccessibilityShortcutChooserActivityTest { private static final String ONE_HANDED_MODE = "One-Handed mode"; private static final String TEST_LABEL = "TEST_LABEL"; private static final ComponentName TEST_COMPONENT_NAME = new ComponentName("package", "class"); + private TestAccessibilityShortcutChooserActivity mActivity; @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @@ -85,10 +90,22 @@ public class AccessibilityShortcutChooserActivityTest { @Mock private IAccessibilityManager mAccessibilityManagerService; + @Before + public void setUp() throws Exception { + when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo); + mResolveInfo.serviceInfo = mServiceInfo; + mServiceInfo.applicationInfo = mApplicationInfo; + when(mResolveInfo.loadLabel(any(PackageManager.class))).thenReturn(TEST_LABEL); + when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME); + when(mAccessibilityManagerService.getInstalledAccessibilityServiceList( + anyInt())).thenReturn(Collections.singletonList(mAccessibilityServiceInfo)); + when(mAccessibilityManagerService.isAccessibilityTargetAllowed( + anyString(), anyInt(), anyInt())).thenReturn(true); + TestAccessibilityShortcutChooserActivity.setupForTesting(mAccessibilityManagerService); + } + @Test - public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist() - throws Exception { - configureTestService(); + public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist() { final ActivityScenario scenario = ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class); scenario.moveToState(Lifecycle.State.CREATED); @@ -101,12 +118,36 @@ public class AccessibilityShortcutChooserActivityTest { onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick()); onView(withId(R.id.accessibility_permission_enable_deny_button)).perform(scrollTo(), click()); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check( doesNotExist()); scenario.moveToState(Lifecycle.State.DESTROYED); } + @Test + public void clickServiceTarget_notPermittedByAdmin_sendRestrictedDialogIntent() + throws Exception { + when(mAccessibilityManagerService.isAccessibilityTargetAllowed( + eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt())).thenReturn(false); + final ActivityScenario scenario = + ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class); + scenario.onActivity(activity -> mActivity = activity); + scenario.moveToState(Lifecycle.State.CREATED); + scenario.moveToState(Lifecycle.State.STARTED); + scenario.moveToState(Lifecycle.State.RESUMED); + + onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot( + isDialog()).check(matches(isDisplayed())); + onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click()); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + onView(withText(TEST_LABEL)).perform(scrollTo(), click()); + verify(mAccessibilityManagerService).sendRestrictedDialogIntent( + eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt()); + scenario.moveToState(Lifecycle.State.DESTROYED); + } + @Test public void popEditShortcutMenuList_oneHandedModeEnabled_shouldBeInListView() { TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true); @@ -145,18 +186,6 @@ public class AccessibilityShortcutChooserActivityTest { scenario.moveToState(Lifecycle.State.DESTROYED); } - private void configureTestService() throws Exception { - when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo); - mResolveInfo.serviceInfo = mServiceInfo; - mServiceInfo.applicationInfo = mApplicationInfo; - when(mResolveInfo.loadLabel(any(PackageManager.class))).thenReturn(TEST_LABEL); - when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME); - when(mAccessibilityManagerService.getInstalledAccessibilityServiceList( - anyInt())).thenReturn(Collections.singletonList(mAccessibilityServiceInfo)); - - TestAccessibilityShortcutChooserActivity.setupForTesting(mAccessibilityManagerService); - } - /** * Used for testing. */ diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 7fba72b74e8d6..26400940bf7d7 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -42,6 +42,7 @@ import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils import static com.android.internal.util.FunctionalUtils.ignoreRemoteException; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.Manifest; import android.accessibilityservice.AccessibilityGestureEvent; @@ -57,8 +58,10 @@ import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.ActivityOptions; import android.app.AlertDialog; +import android.app.AppOpsManager; import android.app.PendingIntent; import android.app.RemoteAction; +import android.app.admin.DevicePolicyManager; import android.appwidget.AppWidgetManagerInternal; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -159,6 +162,7 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; +import com.android.settingslib.RestrictedLockUtils; import org.xmlpull.v1.XmlPullParserException; @@ -3898,6 +3902,51 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + @Override + @RequiresPermission(anyOf = { + android.Manifest.permission.MANAGE_USERS, + android.Manifest.permission.QUERY_ADMIN_POLICY}) + public boolean isAccessibilityTargetAllowed(String packageName, int uid, int userId) { + final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); + final List permittedServices = dpm.getPermittedAccessibilityServices(userId); + + // permittedServices null means all accessibility services are allowed. + boolean allowed = permittedServices == null || permittedServices.contains(packageName); + if (allowed) { + final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class); + final int mode = appOps.noteOpNoThrow( + AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, + uid, packageName, /* attributionTag= */ null, /* message= */ null); + final boolean ecmEnabled = mContext.getResources().getBoolean( + R.bool.config_enhancedConfirmationModeEnabled); + return !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED; + } + return false; + } + + @Override + @RequiresPermission(anyOf = { + android.Manifest.permission.MANAGE_USERS, + android.Manifest.permission.QUERY_ADMIN_POLICY}) + public boolean sendRestrictedDialogIntent(String packageName, int uid, int userId) { + // The accessibility service is allowed. Don't show the restricted dialog. + if (isAccessibilityTargetAllowed(packageName, uid, userId)) { + return false; + } + + final EnforcedAdmin admin = + RestrictedLockUtilsInternal.checkIfAccessibilityServiceDisallowed( + mContext, packageName, userId); + if (admin != null) { + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, admin); + return true; + } + + RestrictedLockUtils.sendShowRestrictedSettingDialogIntent(mContext, + packageName, uid); + return true; + } + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java index 48060017e6d2c..c37ea501bbc9c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java @@ -24,9 +24,7 @@ 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; @@ -46,7 +44,6 @@ import android.view.inputmethod.InputMethodInfo; import com.android.internal.util.ArrayUtils; import com.android.server.inputmethod.InputMethodManagerInternal; -import com.android.settingslib.RestrictedLockUtils; import libcore.util.EmptyArray; @@ -420,7 +417,7 @@ public class AccessibilitySecurityPolicy { // TODO(b/207697949, b/208872785): Add cts test for managed device. // Use RestrictedLockUtilsInternal in AccessibilitySecurityPolicy - if (checkIfInputMethodDisallowed( + if (RestrictedLockUtilsInternal.checkIfInputMethodDisallowed( mContext, inputMethodInfo.getPackageName(), callingUserId) != null) { return ENABLE_IME_FAIL_BY_ADMIN; } @@ -428,72 +425,6 @@ public class AccessibilitySecurityPolicy { 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 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. * diff --git a/services/accessibility/java/com/android/server/accessibility/RestrictedLockUtilsInternal.java b/services/accessibility/java/com/android/server/accessibility/RestrictedLockUtilsInternal.java new file mode 100644 index 0000000000000..28810e6fa5520 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/RestrictedLockUtilsInternal.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 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.accessibility; + +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import android.annotation.UserIdInt; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; + +import com.android.settingslib.RestrictedLockUtils; + +import java.util.List; + +/** + * Utility class to host methods usable to return {@link EnforcedAdmin} instances based on device + * admin policy. + */ +public class RestrictedLockUtilsInternal { + + /** + * Disables accessibility service that are not permitted. + */ + public static EnforcedAdmin checkIfAccessibilityServiceDisallowed(Context context, + String packageName, int userId) { + final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); + if (dpm == null) { + return null; + } + final EnforcedAdmin admin = + RestrictedLockUtils.getProfileOrDeviceOwner(context, getUserHandleOf(userId)); + boolean permitted = true; + if (admin != null) { + permitted = dpm.isAccessibilityServicePermittedByAdmin(admin.component, + packageName, userId); + } + int managedProfileId = getManagedProfileId(context, userId); + final EnforcedAdmin profileAdmin = RestrictedLockUtils.getProfileOrDeviceOwner(context, + getUserHandleOf(managedProfileId)); + boolean permittedByProfileAdmin = true; + if (profileAdmin != null) { + permittedByProfileAdmin = dpm.isAccessibilityServicePermittedByAdmin( + profileAdmin.component, packageName, managedProfileId); + } + if (!permitted && !permittedByProfileAdmin) { + return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN; + } else if (!permitted) { + return admin; + } else if (!permittedByProfileAdmin) { + return profileAdmin; + } + return null; + } + + /** + * Disables input method that are not permitted. + */ + public static EnforcedAdmin checkIfInputMethodDisallowed(Context context, String packageName, + int userId) { + final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); + if (dpm == null) { + return null; + } + final EnforcedAdmin admin = + RestrictedLockUtils.getProfileOrDeviceOwner(context, getUserHandleOf(userId)); + boolean permitted = true; + if (admin != null) { + permitted = dpm.isInputMethodPermittedByAdmin(admin.component, + packageName, userId); + } + + boolean permittedByParentAdmin = true; + 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()) { + final DevicePolicyManager parentDpm = dpm.getParentProfileInstance( + UserManager.get(context).getUserInfo(managedProfileId)); + permittedByParentAdmin = parentDpm.isInputMethodPermittedByAdmin( + profileAdmin.component, packageName, managedProfileId); + } + } + if (!permitted && !permittedByParentAdmin) { + return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN; + } else if (!permitted) { + return admin; + } else if (!permittedByParentAdmin) { + return profileAdmin; + } + return null; + } + + private static int getManagedProfileId(Context context, int userId) { + final UserManager um = context.getSystemService(UserManager.class); + final List userProfiles = um.getProfiles(userId); + for (UserInfo uInfo : userProfiles) { + if (uInfo.id == userId) { + continue; + } + if (uInfo.isManagedProfile()) { + return uInfo.id; + } + } + return UserHandle.USER_NULL; + } + + /** + * @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); + } + } +}