/*
 * Copyright (C) 2019 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.accessibilityservice.AccessibilityService.SoftKeyboardController.ENABLE_IME_FAIL_BY_ADMIN;
import static android.accessibilityservice.AccessibilityService.SoftKeyboardController.ENABLE_IME_SUCCESS;
import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;

import android.Manifest;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.role.RoleManager;
import android.appwidget.AppWidgetManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseBooleanArray;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.InputMethodInfo;

import com.android.internal.util.ArrayUtils;
import com.android.server.inputmethod.InputMethodManagerInternal;

import libcore.util.EmptyArray;

import java.util.ArrayList;
import java.util.List;
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 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 static final int KEEP_SOURCE_EVENT_TYPES = AccessibilityEvent.TYPE_VIEW_CLICKED
            | AccessibilityEvent.TYPE_VIEW_FOCUSED
            | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
            | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
            | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED
            | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
            | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
            | AccessibilityEvent.TYPE_WINDOWS_CHANGED
            | AccessibilityEvent.TYPE_VIEW_SELECTED
            | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
            | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
            | AccessibilityEvent.TYPE_VIEW_SCROLLED
            | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
            | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
            | AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY
            | AccessibilityEvent.TYPE_VIEW_TARGETED_BY_SCROLL;

    /**
     * Methods that should find their way into separate modules, but are still in AMS
     * TODO (b/111889696): Refactoring UserState to AccessibilityUserManager.
     */
    public interface AccessibilityUserManager {
        /**
         * Returns current userId maintained in accessibility manager service
         */
        int getCurrentUserIdLocked();
        // TODO: Should include resolveProfileParentLocked, but that was already in SecurityPolicy

        // TODO(b/255426725): temporary hack; see comment on A11YMS.mVisibleBgUserIds
        /**
         * Returns the {@link android.os.UserManager#getVisibleUsers() visible users}.
         */
        @Nullable SparseBooleanArray getVisibleUserIdsLocked();
    }

    private final Context mContext;
    private final PackageManager mPackageManager;
    private final PackageManagerInternal mPackageManagerInternal;
    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;
    private boolean mSendNonA11yToolNotificationEnabled = false;

    /**
     * Constructor for AccessibilityManagerService.
     */
    public AccessibilitySecurityPolicy(PolicyWarningUIController policyWarningUIController,
            @NonNull Context context,
            @NonNull AccessibilityUserManager a11yUserManager,
            @NonNull PackageManagerInternal packageManagerInternal) {
        mContext = context;
        mAccessibilityUserManager = a11yUserManager;
        mPackageManager = mContext.getPackageManager();
        mPackageManagerInternal = packageManagerInternal;
        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        mPolicyWarningUIController = policyWarningUIController;
    }

    /**
     * Enables sending the notification for non-AccessibilityTool services with the given state.
     *
     */
    public void setSendingNonA11yToolNotificationLocked(boolean enable) {
        if (enable == mSendNonA11yToolNotificationEnabled) {
            return;
        }

        mSendNonA11yToolNotificationEnabled = enable;
        mPolicyWarningUIController.enableSendingNonA11yToolNotification(enable);
        if (enable) {
            for (int i = 0; i < mNonA11yCategoryServices.size(); i++) {
                final ComponentName service = mNonA11yCategoryServices.valueAt(i);
                mPolicyWarningUIController.onNonA11yCategoryServiceBound(mCurrentUserId, service);
            }
        }
    }

    /**
     * Setup AccessibilityWindowManager. This isn't part of the constructor because the
     * window manager and security policy both call each other.
     */
    public void setAccessibilityWindowManager(@NonNull AccessibilityWindowManager awm) {
        mAccessibilityWindowManager = awm;
    }

    /**
     * Setup AppWidgetManger during boot phase.
     */
    public void setAppWidgetManager(@NonNull AppWidgetManagerInternal appWidgetManager) {
        mAppWidgetService = appWidgetManager;
    }

    /**
     * Check if an accessibility event can be dispatched. Events should be dispatched only if they
     * are dispatched from items that services can see.
     *
     * @param userId The userId to check
     * @param event The event to check
     * @return {@code true} if the event can be dispatched
     */
    public boolean canDispatchAccessibilityEventLocked(int userId,
            @NonNull AccessibilityEvent event) {
        final int eventType = event.getEventType();
        switch (eventType) {
            // All events that are for changes in a global window
            // state should *always* be dispatched.
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
            case AccessibilityEvent.TYPE_ANNOUNCEMENT:
                // All events generated by the user touching the
                // screen should *always* be dispatched.
            case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
            case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
            case AccessibilityEvent.TYPE_GESTURE_DETECTION_START:
            case AccessibilityEvent.TYPE_GESTURE_DETECTION_END:
            case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START:
            case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
            case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
            case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:
                // Also always dispatch the event that assist is reading context.
            case AccessibilityEvent.TYPE_ASSIST_READING_CONTEXT:
                // Also windows changing should always be dispatched.
            case AccessibilityEvent.TYPE_WINDOWS_CHANGED: {
                return true;
            }
            // All events for changes in window content should be
            // dispatched *only* if this window is one of the windows
            // the accessibility layer reports which are windows
            // that a sighted user can touch.
            default: {
                return isRetrievalAllowingWindowLocked(userId, event.getWindowId());
            }
        }
    }

    /**
     * Find a valid package name for an app to expose to accessibility
     *
     * @param packageName The package name the app wants to expose
     * @param appId The app's id
     * @param userId The app's user id
     * @param pid The app's process pid that requested this
     * @return A package name that is valid to report
     */
    @Nullable
    public String resolveValidReportedPackageLocked(
            @Nullable CharSequence packageName, int appId, int userId, int pid) {
        // Okay to pass no package
        if (packageName == null) {
            return null;
        }
        // The system gets to pass any package
        if (appId == Process.SYSTEM_UID) {
            return packageName.toString();
        }
        // Passing a package in your UID is fine
        final String packageNameStr = packageName.toString();
        final int resolvedUid = UserHandle.getUid(userId, appId);
        if (isValidPackageForUid(packageNameStr, resolvedUid)) {
            return packageName.toString();
        }
        // Appwidget hosts get to pass packages for widgets they host
        if (mAppWidgetService != null && ArrayUtils.contains(mAppWidgetService
                .getHostedWidgetPackages(resolvedUid), packageNameStr)) {
            return packageName.toString();
        }
        // If app has the targeted permission to act as another package
        if (mContext.checkPermission(Manifest.permission.ACT_AS_PACKAGE_FOR_ACCESSIBILITY,
                pid, resolvedUid) == PackageManager.PERMISSION_GRANTED) {
            return packageName.toString();
        }
        // Otherwise, set the package to the first one in the UID
        final String[] packageNames = mPackageManager.getPackagesForUid(resolvedUid);
        if (ArrayUtils.isEmpty(packageNames)) {
            return null;
        }
        // Okay, the caller reported a package it does not have access to.
        // Instead of crashing the caller for better backwards compatibility
        // we report the first package in the UID. Since most of the time apps
        // don't use shared user id, this will yield correct results and for
        // the edge case of using a shared user id we may report the wrong
        // package but this is fine since first, this is a cheating app and
        // second there is no way to get the correct package anyway.
        return packageNames[0];
    }

    /**
     * Get the packages that are valid for a uid. In some situations, like app widgets, there
     * could be several valid packages
     *
     * @param targetPackage A package that is known to be valid for this id
     * @param targetUid The whose packages should be checked
     * @return An array of all valid package names. An empty array means any package is OK
     */
    @NonNull
    public String[] computeValidReportedPackages(
            @NonNull String targetPackage, int targetUid) {
        if (UserHandle.getAppId(targetUid) == Process.SYSTEM_UID) {
            // Empty array means any package is Okay
            return EmptyArray.STRING;
        }
        // IMPORTANT: The target package is already vetted to be in the target UID
        String[] uidPackages = new String[]{targetPackage};
        // Appwidget hosts get to pass packages for widgets they host
        if (mAppWidgetService != null) {
            final ArraySet<String> widgetPackages = mAppWidgetService
                    .getHostedWidgetPackages(targetUid);
            if (widgetPackages != null && !widgetPackages.isEmpty()) {
                final String[] validPackages = new String[uidPackages.length
                        + widgetPackages.size()];
                System.arraycopy(uidPackages, 0, validPackages, 0, uidPackages.length);
                final int widgetPackageCount = widgetPackages.size();
                for (int i = 0; i < widgetPackageCount; i++) {
                    validPackages[uidPackages.length + i] = widgetPackages.valueAt(i);
                }
                return validPackages;
            }
        }
        return uidPackages;
    }

    /**
     * Reset the event source for events that should not carry one
     *
     * @param event The event potentially to modify
     */
    public void updateEventSourceLocked(@NonNull AccessibilityEvent event) {
        if ((event.getEventType() & KEEP_SOURCE_EVENT_TYPES) == 0) {
            event.setSource(null);
        }
    }

    /**
     * Check if a service can have access to a window
     *
     * @param userId The id of the user running the service
     * @param service The service requesting access
     * @param windowId The window it wants access to
     *
     * @return Whether ot not the service may retrieve info from the window
     */
    public boolean canGetAccessibilityNodeInfoLocked(int userId,
            @NonNull AbstractAccessibilityServiceConnection service, int windowId) {
        return canRetrieveWindowContentLocked(service)
                && isRetrievalAllowingWindowLocked(userId, windowId);
    }

    /**
     * Check if a service can have access the list of windows
     *
     * @param service The service requesting access
     *
     * @return Whether ot not the service may retrieve the window list
     */
    public boolean canRetrieveWindowsLocked(
            @NonNull AbstractAccessibilityServiceConnection service) {
        return canRetrieveWindowContentLocked(service) && service.mRetrieveInteractiveWindows;
    }

    /**
     * Check if a service can have access the content of windows on the screen
     *
     * @param service The service requesting access
     *
     * @return Whether ot not the service may retrieve the content
     */
    public boolean canRetrieveWindowContentLocked(
            @NonNull AbstractAccessibilityServiceConnection service) {
        return (service.getCapabilities()
                & AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0;
    }

    /**
     * Check if a service can control magnification
     *
     * @param service The service requesting access
     *
     * @return Whether ot not the service may control magnification
     */
    public boolean canControlMagnification(
            @NonNull AbstractAccessibilityServiceConnection service) {
        return (service.getCapabilities()
                & AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION) != 0;
    }

    /**
     * Check if a service can perform gestures
     *
     * @param service The service requesting access
     *
     * @return Whether ot not the service may perform gestures
     */
    public boolean canPerformGestures(@NonNull AccessibilityServiceConnection service) {
        return (service.getCapabilities()
                & AccessibilityServiceInfo.CAPABILITY_CAN_PERFORM_GESTURES) != 0;
    }

    /**
     * Check if a service can capture gestures from the fingerprint sensor
     *
     * @param service The service requesting access
     *
     * @return Whether ot not the service may capture gestures from the fingerprint sensor
     */
    public boolean canCaptureFingerprintGestures(@NonNull AccessibilityServiceConnection service) {
        return (service.getCapabilities()
                & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES) != 0;
    }

    /**
     * Checks if a service can take screenshot.
     *
     * @param service The service requesting access
     *
     * @return Whether ot not the service may take screenshot
     */
    public boolean canTakeScreenshotLocked(
            @NonNull AbstractAccessibilityServiceConnection service) {
        return (service.getCapabilities()
                & AccessibilityServiceInfo.CAPABILITY_CAN_TAKE_SCREENSHOT) != 0;
    }

    /**
     * Check whether the input method can be enabled or disabled by the accessibility service.
     *
     * @param imeId The id of the input method.
     * @param service The accessibility service connection.
     * @return Whether the input method can be enabled/disabled or the reason why it can't be
     *         enabled/disabled.
     * @throws SecurityException if the input method is not in the same package as the service.
     */
    @AccessibilityService.SoftKeyboardController.EnableImeResult
    int canEnableDisableInputMethod(String imeId, AbstractAccessibilityServiceConnection service)
            throws SecurityException {
        final String servicePackageName = service.getComponentName().getPackageName();
        final int callingUserId = UserHandle.getCallingUserId();

        InputMethodInfo inputMethodInfo = null;
        List<InputMethodInfo> inputMethodInfoList =
                InputMethodManagerInternal.get().getInputMethodListAsUser(callingUserId);
        if (inputMethodInfoList != null) {
            for (InputMethodInfo info : inputMethodInfoList) {
                if (info.getId().equals(imeId)) {
                    inputMethodInfo = info;
                    break;
                }
            }
        }

        if (inputMethodInfo == null
                || !inputMethodInfo.getPackageName().equals(servicePackageName)) {
            throw new SecurityException("The input method is in a different package with the "
                    + "accessibility service");
        }

        // TODO(b/207697949, b/208872785): Add cts test for managed device.
        //  Use RestrictedLockUtilsInternal in AccessibilitySecurityPolicy
        if (RestrictedLockUtilsInternal.checkIfInputMethodDisallowed(
                mContext, inputMethodInfo.getPackageName(), callingUserId) != null) {
            return ENABLE_IME_FAIL_BY_ADMIN;
        }

        return ENABLE_IME_SUCCESS;
    }

    /**
     * Returns the parent userId of the profile according to the specified userId.
     *
     * @param userId The userId to check
     * @return the parent userId of the profile, or self if no parent exist
     */
    public int resolveProfileParentLocked(int userId) {
        if (userId != mAccessibilityUserManager.getCurrentUserIdLocked()) {
            final long identity = Binder.clearCallingIdentity();
            try {
                UserInfo parent = mUserManager.getProfileParent(userId);
                if (parent != null) {
                    return parent.getUserHandle().getIdentifier();
                }
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
        return userId;
    }

    /**
     * Returns the parent userId of the profile according to the specified userId. Enforcing
     * permissions check if specified userId is not caller's userId.
     *
     * @param userId The userId to check
     * @return the parent userId of the profile, or self if no parent exist
     * @throws SecurityException if caller cannot interact across users
     * @throws IllegalArgumentException if specified invalid userId
     */
    public int resolveCallingUserIdEnforcingPermissionsLocked(int userId) {
        final int callingUid = Binder.getCallingUid();
        final int currentUserId = mAccessibilityUserManager.getCurrentUserIdLocked();
        if (callingUid == 0
                || callingUid == Process.SYSTEM_UID
                || callingUid == Process.SHELL_UID) {
            if (userId == UserHandle.USER_CURRENT
                    || userId == UserHandle.USER_CURRENT_OR_SELF) {
                return currentUserId;
            }
            return resolveProfileParentLocked(userId);
        }
        final int callingUserId = UserHandle.getUserId(callingUid);
        if (callingUserId == userId) {
            return resolveProfileParentLocked(userId);
        }
        final int callingUserParentId = resolveProfileParentLocked(callingUserId);
        if (callingUserParentId == currentUserId && (userId == UserHandle.USER_CURRENT
                || userId == UserHandle.USER_CURRENT_OR_SELF)) {
            return currentUserId;
        }
        if (!hasPermission(Manifest.permission.INTERACT_ACROSS_USERS)
                && !hasPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
            throw new SecurityException("Call from user " + callingUserId + " as user "
                    + userId + " without permission INTERACT_ACROSS_USERS or "
                    + "INTERACT_ACROSS_USERS_FULL not allowed.");
        }
        if (userId == UserHandle.USER_CURRENT
                || userId == UserHandle.USER_CURRENT_OR_SELF) {
            return currentUserId;
        }
        return resolveProfileParentLocked(userId);
    }

    /**
     * Returns false if caller is not SYSTEM and SHELL, and tried to interact across users.
     *
     * @param userId The userId to interact.
     * @return false if caller cannot interact across users.
     */
    public boolean isCallerInteractingAcrossUsers(int userId) {
        final int callingUid = Binder.getCallingUid();
        return (Binder.getCallingPid() == android.os.Process.myPid()
                || callingUid == Process.SHELL_UID
                || userId == UserHandle.USER_CURRENT
                || userId == UserHandle.USER_CURRENT_OR_SELF);
    }

    private boolean isValidPackageForUid(String packageName, int uid) {
        final long token = Binder.clearCallingIdentity();
        try {
            // Since we treat calls from a profile as if made by its parent, using
            // MATCH_ANY_USER to query the uid of the given package name.
            return mPackageManagerInternal.isSameApp(packageName, PackageManager.MATCH_ANY_USER,
                    uid, UserHandle.getUserId(uid));
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    private boolean isRetrievalAllowingWindowLocked(int userId, int windowId) {
        // The system gets to interact with any window it wants.
        if (Binder.getCallingUid() == Process.SYSTEM_UID) {
            return true;
        }
        if (Binder.getCallingUid() == Process.SHELL_UID) {
            if (!isShellAllowedToRetrieveWindowLocked(userId, windowId)) {
                return false;
            }
        }
        if (mAccessibilityWindowManager.resolveParentWindowIdLocked(windowId)
                == mAccessibilityWindowManager.getActiveWindowId(userId)) {
            return true;
        }
        return mAccessibilityWindowManager.findA11yWindowInfoByIdLocked(windowId) != null;
    }

    private boolean isShellAllowedToRetrieveWindowLocked(int userId, int windowId) {
        final long token = Binder.clearCallingIdentity();
        try {
            IBinder windowToken = mAccessibilityWindowManager
                    .getWindowTokenForUserAndWindowIdLocked(userId, windowId);
            if (windowToken == null) {
                return false;
            }
            int windowOwnerUserId = mAccessibilityWindowManager.getWindowOwnerUserId(windowToken);
            if (windowOwnerUserId == UserHandle.USER_NULL) {
                return false;
            }
            return !mUserManager.hasUserRestriction(
                    UserManager.DISALLOW_DEBUGGING_FEATURES, UserHandle.of(windowOwnerUserId));
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    /**
     * Enforcing permission check to caller.
     *
     * @param permission The permission to check
     * @param function The function name to check
     */
    public void enforceCallingPermission(@NonNull String permission, @Nullable String function) {
        if (OWN_PROCESS_ID == Binder.getCallingPid()) {
            return;
        }
        if (!hasPermission(permission)) {
            throw new SecurityException("You do not have " + permission
                    + " required to call " + function + " from pid="
                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
        }
    }

    /**
     * Permission check to caller.
     *
     * @param permission The permission to check
     * @return true if caller has permission
     */
    public boolean hasPermission(@NonNull String permission) {
        return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Checks if accessibility service could register into the system.
     *
     * @param serviceInfo The ServiceInfo
     * @return True if it could register into the system
     */
    public boolean canRegisterService(@NonNull ServiceInfo serviceInfo) {
        if (!android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE.equals(
                serviceInfo.permission)) {
            Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
                    serviceInfo.packageName, serviceInfo.name).flattenToShortString()
                    + ": it does not require the permission "
                    + android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE);
            return false;
        }

        if ((serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) != 0) {
            Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
                    serviceInfo.packageName, serviceInfo.name).flattenToShortString()
                    + ": the service is the external one and doesn't allow to register as "
                    + "an accessibility service ");
            return false;
        }

        int servicePackageUid = serviceInfo.applicationInfo.uid;
        if (mAppOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE,
                servicePackageUid, serviceInfo.packageName, null, null)
                != AppOpsManager.MODE_ALLOWED) {
            Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
                    serviceInfo.packageName, serviceInfo.name).flattenToShortString()
                    + ": disallowed by AppOps");
            return false;
        }

        return true;
    }

    /**
     * Checks if accessibility service could execute accessibility operations.
     *
     * @param service The accessibility service connection
     * @return True if it could execute accessibility operations
     */
    public boolean checkAccessibilityAccess(AbstractAccessibilityServiceConnection service) {
        final String packageName = service.getComponentName().getPackageName();
        final ResolveInfo resolveInfo = service.getServiceInfo().getResolveInfo();

        if (resolveInfo == null) {
            // For InteractionBridge, UiAutomation, and Proxy.
            return true;
        }

        final int servicePackageUid = resolveInfo.serviceInfo.applicationInfo.uid;
        final int callingPid = Binder.getCallingPid();
        final long identityToken = Binder.clearCallingIdentity();
        final String attributionTag = service.getAttributionTag();
        try {
            // For the caller is system, just block the data to a11y services.
            if (OWN_PROCESS_ID == callingPid) {
                return mAppOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_ACCESS_ACCESSIBILITY,
                        servicePackageUid, packageName, attributionTag, null)
                        == AppOpsManager.MODE_ALLOWED;
            }

            return mAppOpsManager.noteOp(AppOpsManager.OPSTR_ACCESS_ACCESSIBILITY,
                    servicePackageUid, packageName, attributionTag, null)
                    == AppOpsManager.MODE_ALLOWED;
        } finally {
            Binder.restoreCallingIdentity(identityToken);
        }
    }

    /**
     * Enforcing permission check to IPC caller or grant it if it's not through IPC.
     *
     * @param permission The permission to check
     */
    public void enforceCallingOrSelfPermission(@NonNull String permission) {
        if (mContext.checkCallingOrSelfPermission(permission)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Caller does not hold permission "
                    + permission);
        }
    }

    /**
     * Throws a SecurityException if the caller has neither the MANAGE_ACCESSIBILITY permission nor
     * the COMPANION_DEVICE_APP_STREAMING role.
     */
    public void checkForAccessibilityPermissionOrRole() {
        final boolean canManageAccessibility =
                mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
                        == PackageManager.PERMISSION_GRANTED;
        if (canManageAccessibility) {
            return;
        }
        final int callingUid = Binder.getCallingUid();
        final long identity = Binder.clearCallingIdentity();
        try {
            final RoleManager roleManager = mContext.getSystemService(RoleManager.class);
            if (roleManager != null) {
                final List<String> holders = roleManager.getRoleHoldersAsUser(
                        DEVICE_PROFILE_APP_STREAMING, UserHandle.getUserHandleForUid(callingUid));
                final String[] packageNames = mPackageManager.getPackagesForUid(callingUid);
                if (packageNames != null) {
                    for (String packageName : packageNames) {
                        if (holders.contains(packageName)) {
                            return;
                        }
                    }
                }
            }
            throw new SecurityException(
                    "Cannot register a proxy for a device without the "
                            + "android.app.role.COMPANION_DEVICE_APP_STREAMING role or the"
                            + " MANAGE_ACCESSIBILITY permission.");
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    /**
     * 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 (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 (!a11yServiceInfo.isAccessibilityTool()) {
                tempNonA11yCategoryServices.add(service);
                if (mNonA11yCategoryServices.contains(service)) {
                    mNonA11yCategoryServices.remove(service);
                } else {
                    if (mSendNonA11yToolNotificationEnabled) {
                        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 (mCurrentUserId == userId) {
            return;
        }
        mPolicyWarningUIController.onSwitchUser(userId,
                new ArraySet<>(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 (mAccessibilityUserManager.getCurrentUserIdLocked() != userId) {
            return;
        }
        mPolicyWarningUIController.onEnabledServicesChanged(userId,
                new ArraySet<>(enabledServices));
    }
}
