summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/provider/Settings.java9
-rw-r--r--core/java/com/android/internal/notification/SystemNotificationChannels.java7
-rw-r--r--core/res/res/drawable/ic_accessibility_24dp.xml27
-rw-r--r--core/res/res/values/strings.xml9
-rw-r--r--core/res/res/values/symbols.xml6
-rw-r--r--proto/src/system_messages.proto4
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java15
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java125
-rw-r--r--services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java370
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/AccessibilitySecurityPolicyTest.java203
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/PolicyWarningUIControllerTest.java241
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;
+ }
+ }
+}