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
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 52bc39c..f0b22a9 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7037,6 +7037,15 @@
             "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 2237efc..2f40d3b 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -58,6 +58,7 @@
     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 @@
         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 0000000..51e6959
--- /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 054d108..0228dfd 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 @@
     <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 ef5191af..567feee3 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 5dd271c..f06a940 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -340,5 +340,9 @@
     // 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 b3be044..e7ffb1a 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 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 @@
         if (isA11yTracingEnabled()) {
             logTrace(LOG_TAG + ".onServiceInfoChangedLocked", "userState=" + userState);
         }
+        mSecurityPolicy.onBoundServicesChangedLocked(userState.mUserId,
+                userState.mBoundServices);
         scheduleNotifyClientsOfServicesStateChangeLocked(userState);
     }
 
@@ -1302,6 +1310,7 @@
             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 @@
                     }
                 } 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 bef6d3e..fd355d8 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 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 @@
             | 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 @@
         // 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 @@
                     + 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 0000000..ea3e650
--- /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 c7e7c78..45f43e8 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.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.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.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 @@
     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 @@
 
     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 @@
             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 @@
             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 @@
             event.setWindowId(WINDOWID);
             assertTrue("Should dispatch [" + event + "]",
                     mA11ySecurityPolicy.canDispatchAccessibilityEventLocked(
-                            UserHandle.USER_SYSTEM,
+                            TEST_USER_ID,
                             event));
         }
     }
@@ -192,24 +222,24 @@
     @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 @@
 
         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 @@
         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 @@
         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 @@
         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 @@
     @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 @@
         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 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 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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 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 0000000..01a641f
--- /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;
+        }
+    }
+}