Prompt notifications for non-accessibility services

Prompts a notification for the non-accessibility category service after
24 hours enabled to alert users the service has powerful permissions to
view and control the device. And the notification won't be resend to
the same service by saving the dismiss record to Settings.

Bug: 176965357
Test: atest AccessibilitySecurityPolicyTest
      atest PolicyWarningUIControllerTest
      and manually test

Change-Id: Id5daf7b14dc88cf3f71a53f46fa9a8f1dee91822
This commit is contained in:
lucychang
2021-01-27 18:05:28 +08:00
committed by Lucy Chang
parent 4e94266881
commit b98a658c27
11 changed files with 946 additions and 70 deletions

View File

@@ -7036,6 +7036,15 @@ public final class Settings {
public static final String ENABLED_ACCESSIBILITY_SERVICES =
"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.

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}
}