diff options
11 files changed, 946 insertions, 70 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 52bc39ce263e..f0b22a923e0f 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7037,6 +7037,15 @@ public final class Settings { "enabled_accessibility_services"; /** + * List of the notified non-accessibility category accessibility services. + * + * @hide + */ + @Readable + public static final String NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES = + "notified_non_accessibility_category_services"; + + /** * List of the accessibility services to which the user has granted * permission to put the device into touch exploration mode. * diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java index 2237efc9e2b6..2f40d3b457c6 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -58,6 +58,7 @@ public class SystemNotificationChannels { public static String SYSTEM_CHANGES = "SYSTEM_CHANGES"; public static String DO_NOT_DISTURB = "DO_NOT_DISTURB"; public static String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION"; + public static String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY"; public static void createAll(Context context) { final NotificationManager nm = context.getSystemService(NotificationManager.class); @@ -199,6 +200,12 @@ public class SystemNotificationChannels { newFeaturePrompt.setBlockable(true); channelsList.add(newFeaturePrompt); + final NotificationChannel accessibilitySecurityPolicyChannel = new NotificationChannel( + ACCESSIBILITY_SECURITY_POLICY, + context.getString(R.string.notification_channel_accessibility_security_policy), + NotificationManager.IMPORTANCE_LOW); + channelsList.add(accessibilitySecurityPolicyChannel); + nm.createNotificationChannels(channelsList); } diff --git a/core/res/res/drawable/ic_accessibility_24dp.xml b/core/res/res/drawable/ic_accessibility_24dp.xml new file mode 100644 index 000000000000..51e695969c85 --- /dev/null +++ b/core/res/res/drawable/ic_accessibility_24dp.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="21dp" + android:height="21dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M20.5,6c-2.61,0.7 -5.67,1 -8.5,1S6.11,6.7 3.5,6L3,8c1.86,0.5 4,0.83 6, + 1v13h2v-6h2v6h2V9c2,-0.17 4.14,-0.5 6,-1L20.5,6zM12,6c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2s-2, + 0.9 -2,2S10.9,6 12,6z" + android:fillColor="#FF000000"/> +</vector>
\ No newline at end of file diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 054d1080f4d4..0228dfd45972 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -743,6 +743,10 @@ magnification. [CHAR_LIMIT=NONE]--> <string name="notification_channel_accessibility_magnification">Magnification</string> + <!-- Text shown when viewing channel settings for notifications related to accessibility + security policy. [CHAR_LIMIT=NONE]--> + <string name="notification_channel_accessibility_security_policy">Accessibility security policy</string> + <!-- Label for foreground service notification when one app is running. [CHAR LIMIT=NONE BACKUP_MESSAGE_ID=6826789589341671842] --> <string name="foreground_service_app_in_background"><xliff:g id="app_name">%1$s</xliff:g> is @@ -5910,4 +5914,9 @@ ul.</string> <string name="splash_screen_view_icon_description">Application icon</string> <!-- Content description for the branding image on the splash screen. [CHAR LIMIT=50] --> <string name="splash_screen_view_branding_description">Application branding image</string> + + <!-- Notification title to prompt the user that some accessibility service has view and control access. [CHAR LIMIT=50] --> + <string name="view_and_control_notification_title">Check access settings</string> + <!-- Notification content to prompt the user that some accessibility service has view and control access. [CHAR LIMIT=none] --> + <string name="view_and_control_notification_content"><xliff:g id="service_name" example="TalkBack">%s</xliff:g> can view and control your screen. Tap to review.</string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ef5191af2e6b..567feee31673 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3541,6 +3541,7 @@ <java-symbol type="string" name="notification_channel_system_changes" /> <java-symbol type="string" name="notification_channel_do_not_disturb" /> <java-symbol type="string" name="notification_channel_accessibility_magnification" /> + <java-symbol type="string" name="notification_channel_accessibility_security_policy" /> <java-symbol type="string" name="config_defaultAutofillService" /> <java-symbol type="string" name="config_defaultOnDeviceSpeechRecognitionService" /> <java-symbol type="string" name="config_defaultTextClassifierPackage" /> @@ -4323,4 +4324,9 @@ <java-symbol type="id" name="remote_views_next_child" /> <java-symbol type="id" name="remote_views_stable_id" /> <java-symbol type="id" name="remote_views_override_id" /> + + <!-- View and control prompt --> + <java-symbol type="drawable" name="ic_accessibility_24dp" /> + <java-symbol type="string" name="view_and_control_notification_title" /> + <java-symbol type="string" name="view_and_control_notification_content" /> </resources> diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index 5dd271c9dbb1..f06a94004110 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -340,5 +340,9 @@ message SystemMessage { // Notify the user that window magnification is available. // package: android NOTE_A11Y_WINDOW_MAGNIFICATION_FEATURE = 1004; + + // Notify the user that some accessibility service has view and control permissions. + // package: android + NOTE_A11Y_VIEW_AND_CONTROL_ACCESS = 1005; } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index b3be0448edaf..e7ffb1a64d4f 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -309,13 +309,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub */ public AccessibilityManagerService(Context context) { mContext = context; - mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mPowerManager = context.getSystemService(PowerManager.class); mWindowManagerService = LocalServices.getService(WindowManagerInternal.class); mA11yController = mWindowManagerService.getAccessibilityController(); mMainHandler = new MainHandler(mContext.getMainLooper()); mActivityTaskManagerService = LocalServices.getService(ActivityTaskManagerInternal.class); mPackageManager = mContext.getPackageManager(); - mSecurityPolicy = new AccessibilitySecurityPolicy(mContext, this); + PolicyWarningUIController policyWarningUIController; + if (AccessibilitySecurityPolicy.POLICY_WARNING_ENABLED) { + policyWarningUIController = new PolicyWarningUIController(mMainHandler, context, + new PolicyWarningUIController.NotificationController(context)); + } + mSecurityPolicy = new AccessibilitySecurityPolicy(policyWarningUIController, mContext, + this); mA11yWindowManager = new AccessibilityWindowManager(mLock, mMainHandler, mWindowManagerService, this, mSecurityPolicy, this); mA11yDisplayListener = new AccessibilityDisplayListener(mContext, mMainHandler); @@ -351,6 +357,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (isA11yTracingEnabled()) { logTrace(LOG_TAG + ".onServiceInfoChangedLocked", "userState=" + userState); } + mSecurityPolicy.onBoundServicesChangedLocked(userState.mUserId, + userState.mBoundServices); scheduleNotifyClientsOfServicesStateChangeLocked(userState); } @@ -1302,6 +1310,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub AccessibilityUserState userState = getCurrentUserStateLocked(); readConfigurationForUserStateLocked(userState); + mSecurityPolicy.onSwitchUserLocked(mCurrentUserId, userState.mEnabledServices); // Even if reading did not yield change, we have to update // the state since the context in which the current user // state was used has changed since it was inactive. @@ -3665,6 +3674,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } else if (mEnabledAccessibilityServicesUri.equals(uri)) { if (readEnabledAccessibilityServicesLocked(userState)) { + mSecurityPolicy.onEnabledServicesChangedLocked(userState.mUserId, + userState.mEnabledServices); userState.updateCrashedServicesIfNeededLocked(); onUserStateChangedLocked(userState); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java index bef6d3e950c1..fd355d8da341 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java @@ -41,21 +41,19 @@ import com.android.internal.util.ArrayUtils; import libcore.util.EmptyArray; +import java.util.ArrayList; +import java.util.Set; + /** * This class provides APIs of accessibility security policies for accessibility manager - * to grant accessibility capabilities or events access right to accessibility service. + * to grant accessibility capabilities or events access right to accessibility services. And also + * monitors the current bound accessibility services to prompt permission warnings for + * not accessibility-categorized ones. */ public class AccessibilitySecurityPolicy { private static final int OWN_PROCESS_ID = android.os.Process.myPid(); private static final String LOG_TAG = "AccessibilitySecurityPolicy"; - private final Context mContext; - private final PackageManager mPackageManager; - private final UserManager mUserManager; - private final AppOpsManager mAppOpsManager; - - private AppWidgetManagerInternal mAppWidgetService; - private static final int KEEP_SOURCE_EVENT_TYPES = AccessibilityEvent.TYPE_VIEW_CLICKED | AccessibilityEvent.TYPE_VIEW_FOCUSED | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER @@ -72,6 +70,8 @@ public class AccessibilitySecurityPolicy { | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED | AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY; + public static final boolean POLICY_WARNING_ENABLED = true; + /** * Methods that should find their way into separate modules, but are still in AMS * TODO (b/111889696): Refactoring UserState to AccessibilityUserManager. @@ -84,19 +84,32 @@ public class AccessibilitySecurityPolicy { // TODO: Should include resolveProfileParentLocked, but that was already in SecurityPolicy } + private final Context mContext; + private final PackageManager mPackageManager; + private final UserManager mUserManager; + private final AppOpsManager mAppOpsManager; private final AccessibilityUserManager mAccessibilityUserManager; + private final PolicyWarningUIController mPolicyWarningUIController; + /** All bound accessibility services which don't belong to accessibility category. */ + private final ArraySet<ComponentName> mNonA11yCategoryServices = new ArraySet<>(); + + private AppWidgetManagerInternal mAppWidgetService; private AccessibilityWindowManager mAccessibilityWindowManager; + private int mCurrentUserId = UserHandle.USER_NULL; /** * Constructor for AccessibilityManagerService. */ - public AccessibilitySecurityPolicy(@NonNull Context context, + public AccessibilitySecurityPolicy(PolicyWarningUIController policyWarningUIController, + @NonNull Context context, @NonNull AccessibilityUserManager a11yUserManager) { mContext = context; mAccessibilityUserManager = a11yUserManager; mPackageManager = mContext.getPackageManager(); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mPolicyWarningUIController = policyWarningUIController; + mPolicyWarningUIController.setAccessibilityPolicyManager(this); } /** @@ -568,4 +581,98 @@ public class AccessibilitySecurityPolicy { + permission); } } + + /** + * Called after a service was bound or unbound. Checks the current bound accessibility + * services and updates alarms. + * + * @param userId The user id + * @param boundServices The bound services + */ + public void onBoundServicesChangedLocked(int userId, + ArrayList<AccessibilityServiceConnection> boundServices) { + if (!POLICY_WARNING_ENABLED) { + return; + } + if (mAccessibilityUserManager.getCurrentUserIdLocked() != userId) { + return; + } + + ArraySet<ComponentName> tempNonA11yCategoryServices = new ArraySet<>(); + for (int i = 0; i < boundServices.size(); i++) { + final AccessibilityServiceInfo a11yServiceInfo = boundServices.get( + i).getServiceInfo(); + final ComponentName service = a11yServiceInfo.getComponentName().clone(); + if (!isA11yCategoryService(a11yServiceInfo)) { + tempNonA11yCategoryServices.add(service); + if (mNonA11yCategoryServices.contains(service)) { + mNonA11yCategoryServices.remove(service); + } else { + mPolicyWarningUIController.onNonA11yCategoryServiceBound(userId, service); + } + } + } + + for (int i = 0; i < mNonA11yCategoryServices.size(); i++) { + final ComponentName service = mNonA11yCategoryServices.valueAt(i); + mPolicyWarningUIController.onNonA11yCategoryServiceUnbound(userId, service); + } + mNonA11yCategoryServices.clear(); + mNonA11yCategoryServices.addAll(tempNonA11yCategoryServices); + } + + /** + * Called after switching to another user. Resets data and cancels old alarms after + * switching to another user. + * + * @param userId The user id + * @param enabledServices The enabled services + */ + public void onSwitchUserLocked(int userId, Set<ComponentName> enabledServices) { + if (!POLICY_WARNING_ENABLED) { + return; + } + if (mCurrentUserId == userId) { + return; + } + + mPolicyWarningUIController.onSwitchUserLocked(userId, enabledServices); + + for (int i = 0; i < mNonA11yCategoryServices.size(); i++) { + mPolicyWarningUIController.onNonA11yCategoryServiceUnbound(mCurrentUserId, + mNonA11yCategoryServices.valueAt(i)); + } + mNonA11yCategoryServices.clear(); + mCurrentUserId = userId; + } + + /** + * Called after the enabled accessibility services changed. + * + * @param userId The user id + * @param enabledServices The enabled services + */ + public void onEnabledServicesChangedLocked(int userId, + Set<ComponentName> enabledServices) { + if (!POLICY_WARNING_ENABLED) { + return; + } + if (mAccessibilityUserManager.getCurrentUserIdLocked() != userId) { + return; + } + + mPolicyWarningUIController.onEnabledServicesChangedLocked(userId, enabledServices); + } + + /** + * Identifies whether the accessibility service is true and designed for accessibility. An + * accessibility service is considered as accessibility category if + * {@link AccessibilityServiceInfo#isAccessibilityTool} is true. + * + * @param serviceInfo The accessibility service's serviceInfo. + * @return Returns true if it is a true accessibility service. + */ + public boolean isA11yCategoryService(AccessibilityServiceInfo serviceInfo) { + return serviceInfo.isAccessibilityTool(); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java b/services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java new file mode 100644 index 000000000000..ea3e650a564a --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2021 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 android.app.AlarmManager.RTC_WAKEUP; + +import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_A11Y_VIEW_AND_CONTROL_ACCESS; +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.Manifest; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.app.ActivityOptions; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.StatusBarManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArraySet; +import android.view.accessibility.AccessibilityManager; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.notification.SystemNotificationChannels; +import com.android.internal.util.ImageUtils; + +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * The class handles permission warning notifications for not accessibility-categorized + * accessibility services from {@link AccessibilitySecurityPolicy}. And also maintains the setting + * {@link Settings.Secure#NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES} in order not to + * resend notifications to the same service. + */ +public class PolicyWarningUIController { + private static final String TAG = PolicyWarningUIController.class.getSimpleName(); + @VisibleForTesting + protected static final String ACTION_SEND_NOTIFICATION = TAG + ".ACTION_SEND_NOTIFICATION"; + @VisibleForTesting + protected static final String ACTION_A11Y_SETTINGS = TAG + ".ACTION_A11Y_SETTINGS"; + @VisibleForTesting + protected static final String ACTION_DISMISS_NOTIFICATION = + TAG + ".ACTION_DISMISS_NOTIFICATION"; + private static final int SEND_NOTIFICATION_DELAY_HOURS = 24; + + /** Current enabled accessibility services. */ + private final ArraySet<ComponentName> mEnabledA11yServices = new ArraySet<>(); + + private final Handler mMainHandler; + private final AlarmManager mAlarmManager; + private final Context mContext; + private final NotificationController mNotificationController; + + public PolicyWarningUIController(@NonNull Handler handler, @NonNull Context context, + NotificationController notificationController) { + mMainHandler = handler; + mContext = context; + mNotificationController = notificationController; + mAlarmManager = mContext.getSystemService(AlarmManager.class); + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_SEND_NOTIFICATION); + filter.addAction(ACTION_A11Y_SETTINGS); + filter.addAction(ACTION_DISMISS_NOTIFICATION); + mContext.registerReceiver(mNotificationController, filter, + Manifest.permission.MANAGE_ACCESSIBILITY, mMainHandler); + + } + + protected void setAccessibilityPolicyManager( + AccessibilitySecurityPolicy accessibilitySecurityPolicy) { + mNotificationController.setAccessibilityPolicyManager(accessibilitySecurityPolicy); + } + + /** + * Updates enabled accessibility services and notified accessibility services after switching + * to another user. + * + * @param enabledServices The current enabled services + */ + public void onSwitchUserLocked(int userId, Set<ComponentName> enabledServices) { + mEnabledA11yServices.clear(); + mEnabledA11yServices.addAll(enabledServices); + mMainHandler.sendMessage(obtainMessage(mNotificationController::onSwitchUser, userId)); + } + + /** + * Computes the newly disabled services and removes its record from the setting + * {@link Settings.Secure#NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES} after detecting the + * setting {@link Settings.Secure#ENABLED_ACCESSIBILITY_SERVICES} changed. + * + * @param userId The user id + * @param enabledServices The enabled services + */ + public void onEnabledServicesChangedLocked(int userId, + Set<ComponentName> enabledServices) { + final ArraySet<ComponentName> disabledServices = new ArraySet<>(mEnabledA11yServices); + disabledServices.removeAll(enabledServices); + mEnabledA11yServices.clear(); + mEnabledA11yServices.addAll(enabledServices); + mMainHandler.sendMessage( + obtainMessage(mNotificationController::onServicesDisabled, userId, + disabledServices)); + } + + /** + * Called when the target service is bound. Sets an 24 hours alarm to the service which is not + * notified yet to execute action {@code ACTION_SEND_NOTIFICATION}. + * + * @param userId The user id + * @param service The service's component name + */ + public void onNonA11yCategoryServiceBound(int userId, ComponentName service) { + mMainHandler.sendMessage(obtainMessage(this::setAlarm, userId, service)); + } + + /** + * Called when the target service is unbound. Cancels the old alarm with intent action + * {@code ACTION_SEND_NOTIFICATION} from the service. + * + * @param userId The user id + * @param service The service's component name + */ + public void onNonA11yCategoryServiceUnbound(int userId, ComponentName service) { + mMainHandler.sendMessage(obtainMessage(this::cancelAlarm, userId, service)); + } + + private void setAlarm(int userId, ComponentName service) { + final Calendar cal = Calendar.getInstance(); + cal.add(Calendar.HOUR, SEND_NOTIFICATION_DELAY_HOURS); + mAlarmManager.set(RTC_WAKEUP, cal.getTimeInMillis(), + createPendingIntent(mContext, userId, ACTION_SEND_NOTIFICATION, + service.flattenToShortString())); + } + + private void cancelAlarm(int userId, ComponentName service) { + mAlarmManager.cancel( + createPendingIntent(mContext, userId, ACTION_SEND_NOTIFICATION, + service.flattenToShortString())); + } + + protected static PendingIntent createPendingIntent(Context context, int userId, String action, + String serviceComponentName) { + final Intent intent = new Intent(action); + intent.setPackage(context.getPackageName()) + .setIdentifier(serviceComponentName) + .putExtra(Intent.EXTRA_USER_ID, userId); + return PendingIntent.getBroadcast(context, 0, intent, + PendingIntent.FLAG_IMMUTABLE); + } + + /** A sub class to handle notifications and settings on the main thread. */ + @MainThread + public static class NotificationController extends BroadcastReceiver { + private static final char RECORD_SEPARATOR = ':'; + + /** All accessibility services which are notified to the user by the policy warning rule. */ + private final ArraySet<ComponentName> mNotifiedA11yServices = new ArraySet<>(); + private final NotificationManager mNotificationManager; + private final Context mContext; + + private int mCurrentUserId; + private AccessibilitySecurityPolicy mAccessibilitySecurityPolicy; + + public NotificationController(Context context) { + mContext = context; + mNotificationManager = mContext.getSystemService(NotificationManager.class); + } + + protected void setAccessibilityPolicyManager( + AccessibilitySecurityPolicy accessibilitySecurityPolicy) { + mAccessibilitySecurityPolicy = accessibilitySecurityPolicy; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final String service = intent.getIdentifier(); + final ComponentName componentName = ComponentName.unflattenFromString(service); + if (TextUtils.isEmpty(action) || TextUtils.isEmpty(service) + || componentName == null) { + return; + } + final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_SYSTEM); + if (ACTION_SEND_NOTIFICATION.equals(action)) { + trySendNotification(userId, componentName); + } else if (ACTION_A11Y_SETTINGS.equals(action)) { + launchSettings(userId, componentName); + mNotificationManager.cancel(service, NOTE_A11Y_VIEW_AND_CONTROL_ACCESS); + onNotificationCanceled(userId, componentName); + } else if (ACTION_DISMISS_NOTIFICATION.equals(action)) { + onNotificationCanceled(userId, componentName); + } + } + + protected void onSwitchUser(int userId) { + mCurrentUserId = userId; + mNotifiedA11yServices.clear(); + mNotifiedA11yServices.addAll(readNotifiedServiceList(userId)); + } + + protected void onServicesDisabled(int userId, + ArraySet<ComponentName> disabledServices) { + if (mNotifiedA11yServices.removeAll(disabledServices)) { + writeNotifiedServiceList(userId, mNotifiedA11yServices); + } + } + + private void trySendNotification(int userId, ComponentName componentName) { + if (!AccessibilitySecurityPolicy.POLICY_WARNING_ENABLED) { + return; + } + if (userId != mCurrentUserId) { + return; + } + + List<AccessibilityServiceInfo> enabledServiceInfos = getEnabledServiceInfos(); + for (int i = 0; i < enabledServiceInfos.size(); i++) { + final AccessibilityServiceInfo a11yServiceInfo = enabledServiceInfos.get(i); + if (componentName.flattenToShortString().equals( + a11yServiceInfo.getComponentName().flattenToShortString())) { + if (!mAccessibilitySecurityPolicy.isA11yCategoryService(a11yServiceInfo) + && !mNotifiedA11yServices.contains(componentName)) { + final CharSequence displayName = + a11yServiceInfo.getResolveInfo().serviceInfo.loadLabel( + mContext.getPackageManager()); + final Drawable drawable = a11yServiceInfo.getResolveInfo().loadIcon( + mContext.getPackageManager()); + final int size = mContext.getResources().getDimensionPixelSize( + android.R.dimen.app_icon_size); + sendNotification(userId, componentName.flattenToShortString(), + displayName, + ImageUtils.buildScaledBitmap(drawable, size, size)); + } + break; + } + } + } + + private void launchSettings(int userId, ComponentName componentName) { + if (userId != mCurrentUserId) { + return; + } + final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName.flattenToShortString()); + final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId( + mContext.getDisplayId()).toBundle(); + mContext.startActivityAsUser(intent, bundle, UserHandle.of(userId)); + mContext.getSystemService(StatusBarManager.class).collapsePanels(); + } + + protected void onNotificationCanceled(int userId, ComponentName componentName) { + if (userId != mCurrentUserId) { + return; + } + + if (mNotifiedA11yServices.add(componentName)) { + writeNotifiedServiceList(userId, mNotifiedA11yServices); + } + } + + private void sendNotification(int userId, String serviceComponentName, CharSequence name, + Bitmap bitmap) { + final Notification.Builder notificationBuilder = new Notification.Builder(mContext, + SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY); + notificationBuilder.setSmallIcon(R.drawable.ic_accessibility_24dp) + .setContentTitle( + mContext.getString(R.string.view_and_control_notification_title)) + .setContentText( + mContext.getString(R.string.view_and_control_notification_content, + name)) + .setStyle(new Notification.BigTextStyle() + .bigText( + mContext.getString( + R.string.view_and_control_notification_content, + name))) + .setTicker(mContext.getString(R.string.view_and_control_notification_title)) + .setOnlyAlertOnce(true) + .setDeleteIntent( + createPendingIntent(mContext, userId, ACTION_DISMISS_NOTIFICATION, + serviceComponentName)) + .setContentIntent( + createPendingIntent(mContext, userId, ACTION_A11Y_SETTINGS, + serviceComponentName)); + if (bitmap != null) { + notificationBuilder.setLargeIcon(bitmap); + } + mNotificationManager.notify(serviceComponentName, NOTE_A11Y_VIEW_AND_CONTROL_ACCESS, + notificationBuilder.build()); + } + + private ArraySet<ComponentName> readNotifiedServiceList(int userId) { + final String notifiedServiceSetting = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES, + userId); + if (TextUtils.isEmpty(notifiedServiceSetting)) { + return new ArraySet<>(); + } + + final TextUtils.StringSplitter componentNameSplitter = + new TextUtils.SimpleStringSplitter(RECORD_SEPARATOR); + componentNameSplitter.setString(notifiedServiceSetting); + + final ArraySet<ComponentName> notifiedServices = new ArraySet<>(); + final Iterator<String> it = componentNameSplitter.iterator(); + while (it.hasNext()) { + final String componentNameString = it.next(); + final ComponentName notifiedService = ComponentName.unflattenFromString( + componentNameString); + if (notifiedService != null) { + notifiedServices.add(notifiedService); + } + } + return notifiedServices; + } + + private void writeNotifiedServiceList(int userId, ArraySet<ComponentName> services) { + StringBuilder notifiedServicesBuilder = new StringBuilder(); + for (int i = 0; i < services.size(); i++) { + if (i > 0) { + notifiedServicesBuilder.append(RECORD_SEPARATOR); + } + final ComponentName notifiedService = services.valueAt(i); + notifiedServicesBuilder.append(notifiedService.flattenToShortString()); + } + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES, + notifiedServicesBuilder.toString(), userId); + } + + @VisibleForTesting + protected List<AccessibilityServiceInfo> getEnabledServiceInfos() { + final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance( + mContext); + return accessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilitySecurityPolicyTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilitySecurityPolicyTest.java index c7e7c7861370..45f43e8b672f 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilitySecurityPolicyTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilitySecurityPolicyTest.java @@ -16,6 +16,8 @@ package com.android.server.accessibility; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + import static junit.framework.Assert.assertFalse; import static junit.framework.TestCase.assertNull; import static junit.framework.TestCase.assertTrue; @@ -30,6 +32,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -47,10 +50,13 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.testing.DexmakerShareClassLoaderRule; +import android.testing.TestableContext; import android.util.ArraySet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityWindowInfo; +import com.android.internal.R; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -58,7 +64,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; /** @@ -72,9 +80,9 @@ public class AccessibilitySecurityPolicyTest { private static final int APP_UID = 10400; private static final int APP_PID = 2000; private static final int SYSTEM_PID = 558; - - private static final String PERMISSION = "test-permission"; - private static final String FUNCTION = "test-function-name"; + private static final int TEST_USER_ID = UserHandle.USER_SYSTEM; + private static final ComponentName TEST_COMPONENT_NAME = new ComponentName( + "com.android.server.accessibility", "AccessibilitySecurityPolicyTest"); private static final int[] ALWAYS_DISPATCH_EVENTS = { AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, @@ -108,29 +116,51 @@ public class AccessibilitySecurityPolicyTest { private AccessibilitySecurityPolicy mA11ySecurityPolicy; + @Rule + public final TestableContext mContext = new TestableContext( + getInstrumentation().getTargetContext(), null); + // To mock package-private class - @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = + @Rule + public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = new DexmakerShareClassLoaderRule(); - @Mock private Context mMockContext; - @Mock private PackageManager mMockPackageManager; - @Mock private UserManager mMockUserManager; - @Mock private AppOpsManager mMockAppOpsManager; - @Mock private AccessibilityServiceConnection mMockA11yServiceConnection; - @Mock private AccessibilityWindowManager mMockA11yWindowManager; - @Mock private AppWidgetManagerInternal mMockAppWidgetManager; - @Mock private AccessibilitySecurityPolicy.AccessibilityUserManager mMockA11yUserManager; + @Mock + private PackageManager mMockPackageManager; + @Mock + private UserManager mMockUserManager; + @Mock + private AppOpsManager mMockAppOpsManager; + @Mock + private AccessibilityServiceConnection mMockA11yServiceConnection; + @Mock + private AccessibilityWindowManager mMockA11yWindowManager; + @Mock + private AppWidgetManagerInternal mMockAppWidgetManager; + @Mock + private AccessibilitySecurityPolicy.AccessibilityUserManager mMockA11yUserManager; + @Mock + private AccessibilityServiceInfo mMockA11yServiceInfo; + @Mock + private PolicyWarningUIController mPolicyWarningUIController; @Before public void setUp() { MockitoAnnotations.initMocks(this); - when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); - when(mMockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager); - when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mMockAppOpsManager); + mContext.setMockPackageManager(mMockPackageManager); + mContext.addMockSystemService(Context.USER_SERVICE, mMockUserManager); + mContext.addMockSystemService(Context.APP_OPS_SERVICE, mMockAppOpsManager); + mContext.getOrCreateTestableResources().addOverride( + R.dimen.accessibility_focus_highlight_stroke_width, 1); - mA11ySecurityPolicy = new AccessibilitySecurityPolicy(mMockContext, mMockA11yUserManager); + when(mMockA11yServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME); + when(mMockA11yServiceConnection.getServiceInfo()).thenReturn(mMockA11yServiceInfo); + + mA11ySecurityPolicy = new AccessibilitySecurityPolicy( + mPolicyWarningUIController, mContext, mMockA11yUserManager); mA11ySecurityPolicy.setAccessibilityWindowManager(mMockA11yWindowManager); mA11ySecurityPolicy.setAppWidgetManager(mMockAppWidgetManager); + mA11ySecurityPolicy.onSwitchUserLocked(TEST_USER_ID, new HashSet<>()); when(mMockA11yWindowManager.resolveParentWindowIdLocked(anyInt())).then(returnsFirstArg()); } @@ -141,7 +171,7 @@ public class AccessibilitySecurityPolicyTest { final AccessibilityEvent event = AccessibilityEvent.obtain(ALWAYS_DISPATCH_EVENTS[i]); assertTrue("Should dispatch [" + event + "]", mA11ySecurityPolicy.canDispatchAccessibilityEventLocked( - UserHandle.USER_SYSTEM, + TEST_USER_ID, event)); } } @@ -154,28 +184,28 @@ public class AccessibilitySecurityPolicyTest { event.setWindowId(invalidWindowId); assertFalse("Shouldn't dispatch [" + event + "]", mA11ySecurityPolicy.canDispatchAccessibilityEventLocked( - UserHandle.USER_SYSTEM, + TEST_USER_ID, event)); } } @Test public void canDispatchAccessibilityEvent_otherEvents_windowIdIsActive_returnTrue() { - when(mMockA11yWindowManager.getActiveWindowId(UserHandle.USER_SYSTEM)) + when(mMockA11yWindowManager.getActiveWindowId(TEST_USER_ID)) .thenReturn(WINDOWID); for (int i = 0; i < OTHER_EVENTS.length; i++) { final AccessibilityEvent event = AccessibilityEvent.obtain(OTHER_EVENTS[i]); event.setWindowId(WINDOWID); assertTrue("Should dispatch [" + event + "]", mA11ySecurityPolicy.canDispatchAccessibilityEventLocked( - UserHandle.USER_SYSTEM, + TEST_USER_ID, event)); } } @Test public void canDispatchAccessibilityEvent_otherEvents_windowIdExist_returnTrue() { - when(mMockA11yWindowManager.getActiveWindowId(UserHandle.USER_SYSTEM)) + when(mMockA11yWindowManager.getActiveWindowId(TEST_USER_ID)) .thenReturn(WINDOWID2); when(mMockA11yWindowManager.findA11yWindowInfoByIdLocked(WINDOWID)) .thenReturn(AccessibilityWindowInfo.obtain()); @@ -184,7 +214,7 @@ public class AccessibilitySecurityPolicyTest { event.setWindowId(WINDOWID); assertTrue("Should dispatch [" + event + "]", mA11ySecurityPolicy.canDispatchAccessibilityEventLocked( - UserHandle.USER_SYSTEM, + TEST_USER_ID, event)); } } @@ -192,24 +222,24 @@ public class AccessibilitySecurityPolicyTest { @Test public void resolveValidReportedPackage_nullPkgName_returnNull() { assertNull(mA11ySecurityPolicy.resolveValidReportedPackageLocked( - null, Process.SYSTEM_UID, UserHandle.USER_SYSTEM, SYSTEM_PID)); + null, Process.SYSTEM_UID, TEST_USER_ID, SYSTEM_PID)); } @Test public void resolveValidReportedPackage_uidIsSystem_returnPkgName() { assertEquals(mA11ySecurityPolicy.resolveValidReportedPackageLocked( - PACKAGE_NAME, Process.SYSTEM_UID, UserHandle.USER_SYSTEM, SYSTEM_PID), + PACKAGE_NAME, Process.SYSTEM_UID, TEST_USER_ID, SYSTEM_PID), PACKAGE_NAME); } @Test public void resolveValidReportedPackage_uidAndPkgNameMatched_returnPkgName() throws PackageManager.NameNotFoundException { - when(mMockPackageManager.getPackageUidAsUser(PACKAGE_NAME, UserHandle.USER_SYSTEM)) + when(mMockPackageManager.getPackageUidAsUser(PACKAGE_NAME, TEST_USER_ID)) .thenReturn(APP_UID); assertEquals(mA11ySecurityPolicy.resolveValidReportedPackageLocked( - PACKAGE_NAME, APP_UID, UserHandle.USER_SYSTEM, APP_PID), + PACKAGE_NAME, APP_UID, TEST_USER_ID, APP_PID), PACKAGE_NAME); } @@ -225,11 +255,11 @@ public class AccessibilitySecurityPolicyTest { when(mMockAppWidgetManager.getHostedWidgetPackages(widgetHostUid)) .thenReturn(widgetPackages); - when(mMockPackageManager.getPackageUidAsUser(hostPackageName, UserHandle.USER_SYSTEM)) + when(mMockPackageManager.getPackageUidAsUser(hostPackageName, TEST_USER_ID)) .thenReturn(widgetHostUid); assertEquals(mA11ySecurityPolicy.resolveValidReportedPackageLocked( - widgetPackageName, widgetHostUid, UserHandle.USER_SYSTEM, widgetHostPid), + widgetPackageName, widgetHostUid, TEST_USER_ID, widgetHostPid), widgetPackageName); } @@ -240,16 +270,16 @@ public class AccessibilitySecurityPolicyTest { final String[] uidPackages = {PACKAGE_NAME, PACKAGE_NAME2}; when(mMockPackageManager.getPackagesForUid(APP_UID)) .thenReturn(uidPackages); - when(mMockPackageManager.getPackageUidAsUser(invalidPackageName, UserHandle.USER_SYSTEM)) + when(mMockPackageManager.getPackageUidAsUser(invalidPackageName, TEST_USER_ID)) .thenThrow(PackageManager.NameNotFoundException.class); when(mMockAppWidgetManager.getHostedWidgetPackages(APP_UID)) .thenReturn(new ArraySet<>()); - when(mMockContext.checkPermission( - eq(Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY), anyInt(), eq(APP_UID))) - .thenReturn(PackageManager.PERMISSION_DENIED); + mContext.getTestablePermissions().setPermission( + Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY, + PackageManager.PERMISSION_DENIED); assertEquals(PACKAGE_NAME, mA11ySecurityPolicy.resolveValidReportedPackageLocked( - invalidPackageName, APP_UID, UserHandle.USER_SYSTEM, APP_PID)); + invalidPackageName, APP_UID, TEST_USER_ID, APP_PID)); } @Test @@ -260,16 +290,16 @@ public class AccessibilitySecurityPolicyTest { final String[] uidPackages = {PACKAGE_NAME}; when(mMockPackageManager.getPackagesForUid(APP_UID)) .thenReturn(uidPackages); - when(mMockPackageManager.getPackageUidAsUser(wantedPackageName, UserHandle.USER_SYSTEM)) + when(mMockPackageManager.getPackageUidAsUser(wantedPackageName, TEST_USER_ID)) .thenReturn(wantedUid); when(mMockAppWidgetManager.getHostedWidgetPackages(APP_UID)) .thenReturn(new ArraySet<>()); - when(mMockContext.checkPermission( - eq(Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY), anyInt(), eq(APP_UID))) - .thenReturn(PackageManager.PERMISSION_GRANTED); + mContext.getTestablePermissions().setPermission( + Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY, + PackageManager.PERMISSION_GRANTED); assertEquals(wantedPackageName, mA11ySecurityPolicy.resolveValidReportedPackageLocked( - wantedPackageName, APP_UID, UserHandle.USER_SYSTEM, APP_PID)); + wantedPackageName, APP_UID, TEST_USER_ID, APP_PID)); } @Test @@ -280,16 +310,16 @@ public class AccessibilitySecurityPolicyTest { final String[] uidPackages = {PACKAGE_NAME}; when(mMockPackageManager.getPackagesForUid(APP_UID)) .thenReturn(uidPackages); - when(mMockPackageManager.getPackageUidAsUser(wantedPackageName, UserHandle.USER_SYSTEM)) + when(mMockPackageManager.getPackageUidAsUser(wantedPackageName, TEST_USER_ID)) .thenReturn(wantedUid); when(mMockAppWidgetManager.getHostedWidgetPackages(APP_UID)) .thenReturn(new ArraySet<>()); - when(mMockContext.checkPermission( - eq(Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY), anyInt(), eq(APP_UID))) - .thenReturn(PackageManager.PERMISSION_DENIED); + mContext.getTestablePermissions().setPermission( + Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY, + PackageManager.PERMISSION_DENIED); assertEquals(PACKAGE_NAME, mA11ySecurityPolicy.resolveValidReportedPackageLocked( - wantedPackageName, APP_UID, UserHandle.USER_SYSTEM, APP_PID)); + wantedPackageName, APP_UID, TEST_USER_ID, APP_PID)); } @Test @@ -301,7 +331,7 @@ public class AccessibilitySecurityPolicyTest { @Test public void computeValidReportedPackages_uidIsAppWidgetHost_returnTargetAndWidgetName() { final int widgetHostUid = APP_UID; - final String targetPackageName = PACKAGE_NAME; + final String targetPackageName = PACKAGE_NAME; final String widgetPackageName = PACKAGE_NAME2; final ArraySet<String> widgetPackages = new ArraySet<>(); widgetPackages.add(widgetPackageName); @@ -320,7 +350,7 @@ public class AccessibilitySecurityPolicyTest { when(mMockA11yServiceConnection.getCapabilities()) .thenReturn(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT); - assertFalse(mA11ySecurityPolicy.canGetAccessibilityNodeInfoLocked(UserHandle.USER_SYSTEM, + assertFalse(mA11ySecurityPolicy.canGetAccessibilityNodeInfoLocked(TEST_USER_ID, mMockA11yServiceConnection, invalidWindowId)); } @@ -328,10 +358,10 @@ public class AccessibilitySecurityPolicyTest { public void canGetAccessibilityNodeInfo_hasCapAndWindowIsActive_returnTrue() { when(mMockA11yServiceConnection.getCapabilities()) .thenReturn(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT); - when(mMockA11yWindowManager.getActiveWindowId(UserHandle.USER_SYSTEM)) + when(mMockA11yWindowManager.getActiveWindowId(TEST_USER_ID)) .thenReturn(WINDOWID); - assertTrue(mA11ySecurityPolicy.canGetAccessibilityNodeInfoLocked(UserHandle.USER_SYSTEM, + assertTrue(mA11ySecurityPolicy.canGetAccessibilityNodeInfoLocked(TEST_USER_ID, mMockA11yServiceConnection, WINDOWID)); } @@ -339,12 +369,12 @@ public class AccessibilitySecurityPolicyTest { public void canGetAccessibilityNodeInfo_hasCapAndWindowExist_returnTrue() { when(mMockA11yServiceConnection.getCapabilities()) .thenReturn(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT); - when(mMockA11yWindowManager.getActiveWindowId(UserHandle.USER_SYSTEM)) + when(mMockA11yWindowManager.getActiveWindowId(TEST_USER_ID)) .thenReturn(WINDOWID2); when(mMockA11yWindowManager.findA11yWindowInfoByIdLocked(WINDOWID)) .thenReturn(AccessibilityWindowInfo.obtain()); - assertTrue(mA11ySecurityPolicy.canGetAccessibilityNodeInfoLocked(UserHandle.USER_SYSTEM, + assertTrue(mA11ySecurityPolicy.canGetAccessibilityNodeInfoLocked(TEST_USER_ID, mMockA11yServiceConnection, WINDOWID)); } @@ -464,8 +494,10 @@ public class AccessibilitySecurityPolicyTest { .thenReturn(currentUserId); doReturn(callingParentId).when(spySecurityPolicy).resolveProfileParentLocked( callingUserId); - when(mMockContext.checkCallingPermission(any())) - .thenReturn(PackageManager.PERMISSION_DENIED); + mContext.getTestablePermissions().setPermission(Manifest.permission.INTERACT_ACROSS_USERS, + PackageManager.PERMISSION_DENIED); + mContext.getTestablePermissions().setPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL, PackageManager.PERMISSION_DENIED); spySecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked( UserHandle.USER_CURRENT_OR_SELF); @@ -482,8 +514,8 @@ public class AccessibilitySecurityPolicyTest { .thenReturn(currentUserId); doReturn(callingParentId).when(spySecurityPolicy).resolveProfileParentLocked( callingUserId); - when(mMockContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS)) - .thenReturn(PackageManager.PERMISSION_GRANTED); + mContext.getTestablePermissions().setPermission(Manifest.permission.INTERACT_ACROSS_USERS, + PackageManager.PERMISSION_GRANTED); assertEquals(wantedUserId, spySecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked(wantedUserId)); @@ -500,8 +532,8 @@ public class AccessibilitySecurityPolicyTest { .thenReturn(currentUserId); doReturn(callingParentId).when(spySecurityPolicy).resolveProfileParentLocked( callingUserId); - when(mMockContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) - .thenReturn(PackageManager.PERMISSION_GRANTED); + mContext.getTestablePermissions().setPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL, PackageManager.PERMISSION_GRANTED); assertEquals(wantedUserId, spySecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked(wantedUserId)); @@ -518,10 +550,10 @@ public class AccessibilitySecurityPolicyTest { .thenReturn(currentUserId); doReturn(callingParentId).when(spySecurityPolicy).resolveProfileParentLocked( callingUserId); - when(mMockContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS)) - .thenReturn(PackageManager.PERMISSION_DENIED); - when(mMockContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) - .thenReturn(PackageManager.PERMISSION_DENIED); + mContext.getTestablePermissions().setPermission(Manifest.permission.INTERACT_ACROSS_USERS, + PackageManager.PERMISSION_DENIED); + mContext.getTestablePermissions().setPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL, PackageManager.PERMISSION_DENIED); spySecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked(wantedUserId); } @@ -562,4 +594,57 @@ public class AccessibilitySecurityPolicyTest { APP_UID, PACKAGE_NAME); } + @Test + public void onBoundServicesChanged_bindA11yCategoryService_noUIControllerAction() { + final ArrayList<AccessibilityServiceConnection> boundServices = new ArrayList<>(); + boundServices.add(mMockA11yServiceConnection); + when(mMockA11yServiceInfo.isAccessibilityTool()).thenReturn(true); + + mA11ySecurityPolicy.onBoundServicesChangedLocked(TEST_USER_ID, boundServices); + + verify(mPolicyWarningUIController, never()).onNonA11yCategoryServiceBound(anyInt(), any()); + } + + @Test + public void onBoundServicesChanged_unbindA11yCategoryService_noUIControllerAction() { + onBoundServicesChanged_bindA11yCategoryService_noUIControllerAction(); + + mA11ySecurityPolicy.onBoundServicesChangedLocked(TEST_USER_ID, new ArrayList<>()); + + verify(mPolicyWarningUIController, never()).onNonA11yCategoryServiceUnbound(anyInt(), + any()); + } + + @Test + public void onBoundServicesChanged_bindNonA11yCategoryService_activateUIControllerAction() { + final ArrayList<AccessibilityServiceConnection> boundServices = new ArrayList<>(); + boundServices.add(mMockA11yServiceConnection); + when(mMockA11yServiceInfo.isAccessibilityTool()).thenReturn(false); + + mA11ySecurityPolicy.onBoundServicesChangedLocked(TEST_USER_ID, boundServices); + + verify(mPolicyWarningUIController).onNonA11yCategoryServiceBound(eq(TEST_USER_ID), + eq(TEST_COMPONENT_NAME)); + } + + @Test + public void onBoundServicesChanged_unbindNonA11yCategoryService_activateUIControllerAction() { + onBoundServicesChanged_bindNonA11yCategoryService_activateUIControllerAction(); + + mA11ySecurityPolicy.onBoundServicesChangedLocked(TEST_USER_ID, new ArrayList<>()); + + verify(mPolicyWarningUIController).onNonA11yCategoryServiceUnbound(eq(TEST_USER_ID), + eq(TEST_COMPONENT_NAME)); + } + + @Test + public void onSwitchUser_differentUser_activateUIControllerAction() { + onBoundServicesChanged_bindNonA11yCategoryService_activateUIControllerAction(); + + mA11ySecurityPolicy.onSwitchUserLocked(2, new HashSet<>()); + + verify(mPolicyWarningUIController).onSwitchUserLocked(eq(2), eq(new HashSet<>())); + verify(mPolicyWarningUIController).onNonA11yCategoryServiceUnbound(eq(TEST_USER_ID), + eq(TEST_COMPONENT_NAME)); + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/PolicyWarningUIControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/PolicyWarningUIControllerTest.java new file mode 100644 index 000000000000..01a641f141ad --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/PolicyWarningUIControllerTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2021 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 android.app.AlarmManager.RTC_WAKEUP; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_A11Y_VIEW_AND_CONTROL_ACCESS; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.Assert.assertEquals; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.StatusBarManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.provider.Settings; +import android.testing.TestableContext; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Tests for the {@link PolicyWarningUIController}. + */ +public class PolicyWarningUIControllerTest { + private static final int TEST_USER_ID = UserHandle.USER_SYSTEM; + private static final ComponentName TEST_COMPONENT_NAME = new ComponentName( + "com.android.server.accessibility", "PolicyWarningUIControllerTest"); + + private final List<AccessibilityServiceInfo> mEnabledServiceList = new ArrayList<>(); + + @Rule + public final A11yTestableContext mContext = new A11yTestableContext( + getInstrumentation().getTargetContext()); + @Mock + private AlarmManager mAlarmManager; + @Mock + private NotificationManager mNotificationManager; + @Mock + private StatusBarManager mStatusBarManager; + @Mock + private AccessibilityServiceInfo mMockA11yServiceInfo; + @Mock + private ResolveInfo mMockResolveInfo; + @Mock + private ServiceInfo mMockServiceInfo; + @Mock + private Context mSpyContext; + @Mock + private AccessibilitySecurityPolicy mAccessibilitySecurityPolicy; + + private PolicyWarningUIController mPolicyWarningUIController; + private FakeNotificationController mFakeNotificationController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext.addMockSystemService(AlarmManager.class, mAlarmManager); + mContext.addMockSystemService(NotificationManager.class, mNotificationManager); + mContext.addMockSystemService(StatusBarManager.class, mStatusBarManager); + mFakeNotificationController = new FakeNotificationController(mContext); + mPolicyWarningUIController = new PolicyWarningUIController( + getInstrumentation().getTargetContext().getMainThreadHandler(), mContext, + mFakeNotificationController); + mPolicyWarningUIController.setAccessibilityPolicyManager(mAccessibilitySecurityPolicy); + mPolicyWarningUIController.onSwitchUserLocked(TEST_USER_ID, new HashSet<>()); + mEnabledServiceList.clear(); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES, + "", TEST_USER_ID); + } + + @Test + public void receiveActionSendNotification_isNonA11yCategoryService_sendNotification() { + mEnabledServiceList.add(mMockA11yServiceInfo); + mMockResolveInfo.serviceInfo = mMockServiceInfo; + when(mMockA11yServiceInfo.getResolveInfo()).thenReturn(mMockResolveInfo); + when(mMockA11yServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME); + when(mAccessibilitySecurityPolicy.isA11yCategoryService( + mMockA11yServiceInfo)).thenReturn(false); + + mFakeNotificationController.onReceive(mContext, createIntent(TEST_USER_ID, + PolicyWarningUIController.ACTION_SEND_NOTIFICATION, + TEST_COMPONENT_NAME.flattenToShortString())); + + verify(mNotificationManager).notify(eq(TEST_COMPONENT_NAME.flattenToShortString()), + eq(NOTE_A11Y_VIEW_AND_CONTROL_ACCESS), any( + Notification.class)); + } + + @Test + public void receiveActionA11ySettings_launchA11ySettingsAndDismissNotification() { + mFakeNotificationController.onReceive(mContext, + createIntent(TEST_USER_ID, PolicyWarningUIController.ACTION_A11Y_SETTINGS, + TEST_COMPONENT_NAME.flattenToShortString())); + + verifyLaunchA11ySettings(); + verify(mNotificationManager).cancel(TEST_COMPONENT_NAME.flattenToShortString(), + NOTE_A11Y_VIEW_AND_CONTROL_ACCESS); + assertNotifiedSettingsEqual(TEST_USER_ID, + TEST_COMPONENT_NAME.flattenToShortString()); + } + + @Test + public void receiveActionDismissNotification_addToNotifiedSettings() { + mFakeNotificationController.onReceive(mContext, createIntent(TEST_USER_ID, + PolicyWarningUIController.ACTION_DISMISS_NOTIFICATION, + TEST_COMPONENT_NAME.flattenToShortString())); + + assertNotifiedSettingsEqual(TEST_USER_ID, + TEST_COMPONENT_NAME.flattenToShortString()); + } + + @Test + public void onEnabledServicesChangedLocked_serviceDisabled_removedFromNotifiedSettings() { + final Set<ComponentName> enabledServices = new HashSet<>(); + enabledServices.add(TEST_COMPONENT_NAME); + mPolicyWarningUIController.onEnabledServicesChangedLocked(TEST_USER_ID, enabledServices); + getInstrumentation().waitForIdleSync(); + receiveActionDismissNotification_addToNotifiedSettings(); + + mPolicyWarningUIController.onEnabledServicesChangedLocked(TEST_USER_ID, new HashSet<>()); + getInstrumentation().waitForIdleSync(); + + assertNotifiedSettingsEqual(TEST_USER_ID, ""); + } + + @Test + public void onNonA11yCategoryServiceBound_setAlarm() { + mPolicyWarningUIController.onNonA11yCategoryServiceBound(TEST_USER_ID, TEST_COMPONENT_NAME); + getInstrumentation().waitForIdleSync(); + + verify(mAlarmManager).set(eq(RTC_WAKEUP), anyLong(), + eq(PolicyWarningUIController.createPendingIntent(mContext, TEST_USER_ID, + PolicyWarningUIController.ACTION_SEND_NOTIFICATION, + TEST_COMPONENT_NAME.flattenToShortString()))); + } + + @Test + public void onNonA11yCategoryServiceUnbound_cancelAlarm() { + mPolicyWarningUIController.onNonA11yCategoryServiceUnbound(TEST_USER_ID, + TEST_COMPONENT_NAME); + getInstrumentation().waitForIdleSync(); + + verify(mAlarmManager).cancel( + eq(PolicyWarningUIController.createPendingIntent(mContext, TEST_USER_ID, + PolicyWarningUIController.ACTION_SEND_NOTIFICATION, + TEST_COMPONENT_NAME.flattenToShortString()))); + } + + private void assertNotifiedSettingsEqual(int userId, String settingString) { + final String notifiedServicesSetting = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES, + userId); + assertEquals(settingString, notifiedServicesSetting); + } + + private Intent createIntent(int userId, String action, String serviceComponentName) { + final Intent intent = new Intent(action); + intent.setPackage(mContext.getPackageName()) + .setIdentifier(serviceComponentName) + .putExtra(Intent.EXTRA_USER_ID, userId); + return intent; + } + + private void verifyLaunchA11ySettings() { + final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + final ArgumentCaptor<UserHandle> userHandleCaptor = ArgumentCaptor.forClass( + UserHandle.class); + verify(mSpyContext).startActivityAsUser(intentCaptor.capture(), + any(), userHandleCaptor.capture()); + assertThat(intentCaptor.getValue().getAction()).isEqualTo( + Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS); + assertThat(userHandleCaptor.getValue().getIdentifier()).isEqualTo(TEST_USER_ID); + verify(mStatusBarManager).collapsePanels(); + } + + private class A11yTestableContext extends TestableContext { + A11yTestableContext(Context base) { + super(base); + } + + @Override + public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { + mSpyContext.startActivityAsUser(intent, options, user); + } + } + + private class FakeNotificationController extends + PolicyWarningUIController.NotificationController { + FakeNotificationController(Context context) { + super(context); + } + + @Override + protected List<AccessibilityServiceInfo> getEnabledServiceInfos() { + return mEnabledServiceList; + } + } +} |