Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2154f683 authored by Daniel Norman's avatar Daniel Norman Committed by Android (Google) Code Review
Browse files

Merge "Parses accessibility packages without holding A11yManagerService#mLock." into main

parents d7400cd8 618169b9
Loading
Loading
Loading
Loading
+7 −0
Original line number Original line Diff line number Diff line
@@ -48,3 +48,10 @@ flag {
    description: "Stops using the deprecated PackageListObserver."
    description: "Stops using the deprecated PackageListObserver."
    bug: "304561459"
    bug: "304561459"
}
}

flag {
    name: "scan_packages_without_lock"
    namespace: "accessibility"
    description: "Scans packages for accessibility service/activity info without holding the A11yMS lock"
    bug: "295969873"
}
+151 −41
Original line number Original line Diff line number Diff line
@@ -290,14 +290,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub


    private final Set<ComponentName> mTempComponentNameSet = new HashSet<>();
    private final Set<ComponentName> mTempComponentNameSet = new HashSet<>();


    private final List<AccessibilityServiceInfo> mTempAccessibilityServiceInfoList =
            new ArrayList<>();

    private final IntArray mTempIntArray = new IntArray(0);
    private final IntArray mTempIntArray = new IntArray(0);


    private final RemoteCallbackList<IAccessibilityManagerClient> mGlobalClients =
    private final RemoteCallbackList<IAccessibilityManagerClient> mGlobalClients =
            new RemoteCallbackList<>();
            new RemoteCallbackList<>();


    private PackageMonitor mPackageMonitor;

    @VisibleForTesting
    @VisibleForTesting
    final SparseArray<AccessibilityUserState> mUserStates = new SparseArray<>();
    final SparseArray<AccessibilityUserState> mUserStates = new SparseArray<>();


@@ -531,6 +530,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        disableAccessibilityMenuToMigrateIfNeeded();
        disableAccessibilityMenuToMigrateIfNeeded();
    }
    }


    /**
     * Returns if the current thread is holding {@link #mLock}. Used for testing
     * deadlock bug fixes.
     *
     * <p><strong>Warning:</strong> this should not be used for production logic
     * because by the time you receive an answer it may no longer be valid.
     * </p>
     */
    @VisibleForTesting
    boolean unsafeIsLockHeld() {
        return Thread.holdsLock(mLock);
    }

    @Override
    @Override
    public int getCurrentUserIdLocked() {
    public int getCurrentUserIdLocked() {
        return mCurrentUserId;
        return mCurrentUserId;
@@ -690,6 +702,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        }
        }
    }
    }


    private void onSomePackagesChangedLocked(
            @Nullable List<AccessibilityServiceInfo> parsedAccessibilityServiceInfos,
            @Nullable List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos) {
        final AccessibilityUserState userState = getCurrentUserStateLocked();
        // Reload the installed services since some services may have different attributes
        // or resolve info (does not support equals), etc. Remove them then to force reload.
        userState.mInstalledServices.clear();
        if (readConfigurationForUserStateLocked(userState,
                    parsedAccessibilityServiceInfos, parsedAccessibilityShortcutInfos)) {
            onUserStateChangedLocked(userState);
        }
    }

    private void onPackageRemovedLocked(String packageName) {
    private void onPackageRemovedLocked(String packageName) {
        final AccessibilityUserState userState = getCurrentUserState();
        final AccessibilityUserState userState = getCurrentUserState();
        final Predicate<ComponentName> filter =
        final Predicate<ComponentName> filter =
@@ -721,8 +746,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        }
        }
    }
    }


    @VisibleForTesting
    PackageMonitor getPackageMonitor() {
        return mPackageMonitor;
    }

    private void registerBroadcastReceivers() {
    private void registerBroadcastReceivers() {
        PackageMonitor monitor = new PackageMonitor() {
        mPackageMonitor = new PackageMonitor() {
            @Override
            @Override
            public void onSomePackagesChanged() {
            public void onSomePackagesChanged() {
                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_PACKAGE_BROADCAST_RECEIVER)) {
                if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_PACKAGE_BROADCAST_RECEIVER)) {
@@ -730,15 +760,27 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
                            FLAGS_PACKAGE_BROADCAST_RECEIVER);
                            FLAGS_PACKAGE_BROADCAST_RECEIVER);
                }
                }


                final int userId = getChangingUserId();
                List<AccessibilityServiceInfo> parsedAccessibilityServiceInfos = null;
                List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos = null;
                if (Flags.scanPackagesWithoutLock()) {
                    parsedAccessibilityServiceInfos = parseAccessibilityServiceInfos(userId);
                    parsedAccessibilityShortcutInfos = parseAccessibilityShortcutInfos(userId);
                }
                synchronized (mLock) {
                synchronized (mLock) {
                    // Only the profile parent can install accessibility services.
                    // Only the profile parent can install accessibility services.
                    // Therefore we ignore packages from linked profiles.
                    // Therefore we ignore packages from linked profiles.
                    if (getChangingUserId() != mCurrentUserId) {
                    if (userId != mCurrentUserId) {
                        return;
                        return;
                    }
                    }
                    if (Flags.scanPackagesWithoutLock()) {
                        onSomePackagesChangedLocked(parsedAccessibilityServiceInfos,
                                parsedAccessibilityShortcutInfos);
                    } else {
                        onSomePackagesChangedLocked();
                        onSomePackagesChangedLocked();
                    }
                    }
                }
                }
            }


            @Override
            @Override
            public void onPackageUpdateFinished(String packageName, int uid) {
            public void onPackageUpdateFinished(String packageName, int uid) {
@@ -751,8 +793,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
                            FLAGS_PACKAGE_BROADCAST_RECEIVER,
                            FLAGS_PACKAGE_BROADCAST_RECEIVER,
                            "packageName=" + packageName + ";uid=" + uid);
                            "packageName=" + packageName + ";uid=" + uid);
                }
                }
                synchronized (mLock) {
                final int userId = getChangingUserId();
                final int userId = getChangingUserId();
                List<AccessibilityServiceInfo> parsedAccessibilityServiceInfos = null;
                List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos = null;
                if (Flags.scanPackagesWithoutLock()) {
                    parsedAccessibilityServiceInfos = parseAccessibilityServiceInfos(userId);
                    parsedAccessibilityShortcutInfos = parseAccessibilityShortcutInfos(userId);
                }
                synchronized (mLock) {
                    if (userId != mCurrentUserId) {
                    if (userId != mCurrentUserId) {
                        return;
                        return;
                    }
                    }
@@ -765,8 +813,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
                    // Reloads the installed services info to make sure the rebound service could
                    // Reloads the installed services info to make sure the rebound service could
                    // get a new one.
                    // get a new one.
                    userState.mInstalledServices.clear();
                    userState.mInstalledServices.clear();
                    final boolean configurationChanged =
                    final boolean configurationChanged;
                            readConfigurationForUserStateLocked(userState);
                    if (Flags.scanPackagesWithoutLock()) {
                        configurationChanged = readConfigurationForUserStateLocked(userState,
                                parsedAccessibilityServiceInfos, parsedAccessibilityShortcutInfos);
                    } else {
                        configurationChanged = readConfigurationForUserStateLocked(userState);
                    }
                    if (reboundAService || configurationChanged) {
                    if (reboundAService || configurationChanged) {
                        onUserStateChangedLocked(userState);
                        onUserStateChangedLocked(userState);
                    }
                    }
@@ -839,7 +892,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        };
        };


        // package changes
        // package changes
        monitor.register(mContext, null,  UserHandle.ALL, true);
        mPackageMonitor.register(mContext, null,  UserHandle.ALL, true);


        if (!Flags.deprecatePackageListObserver()) {
        if (!Flags.deprecatePackageListObserver()) {
            final PackageManagerInternal pm = LocalServices.getService(
            final PackageManagerInternal pm = LocalServices.getService(
@@ -1831,8 +1884,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        mA11yWindowManager.onTouchInteractionEnd();
        mA11yWindowManager.onTouchInteractionEnd();
    }
    }


    private void switchUser(int userId) {
    @VisibleForTesting
    void switchUser(int userId) {
        mMagnificationController.updateUserIdIfNeeded(userId);
        mMagnificationController.updateUserIdIfNeeded(userId);
        List<AccessibilityServiceInfo> parsedAccessibilityServiceInfos = null;
        List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos = null;
        if (Flags.scanPackagesWithoutLock()) {
            parsedAccessibilityServiceInfos = parseAccessibilityServiceInfos(userId);
            parsedAccessibilityShortcutInfos = parseAccessibilityShortcutInfos(userId);
        }
        synchronized (mLock) {
        synchronized (mLock) {
            if (mCurrentUserId == userId && mInitialized) {
            if (mCurrentUserId == userId && mInitialized) {
                return;
                return;
@@ -1857,7 +1917,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
            mCurrentUserId = userId;
            mCurrentUserId = userId;
            AccessibilityUserState userState = getCurrentUserStateLocked();
            AccessibilityUserState userState = getCurrentUserStateLocked();


            if (Flags.scanPackagesWithoutLock()) {
                readConfigurationForUserStateLocked(userState,
                        parsedAccessibilityServiceInfos, parsedAccessibilityShortcutInfos);
            } else {
                readConfigurationForUserStateLocked(userState);
                readConfigurationForUserStateLocked(userState);
            }
            mSecurityPolicy.onSwitchUserLocked(mCurrentUserId, userState.mEnabledServices);
            mSecurityPolicy.onSwitchUserLocked(mCurrentUserId, userState.mEnabledServices);
            // Even if reading did not yield change, we have to update
            // Even if reading did not yield change, we have to update
            // the state since the context in which the current user
            // the state since the context in which the current user
@@ -2105,8 +2170,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        }
        }
    }
    }


    private boolean readInstalledAccessibilityServiceLocked(AccessibilityUserState userState) {
    /**
        mTempAccessibilityServiceInfoList.clear();
     * Finds packages that provide AccessibilityService interfaces, and parses
     * their metadata XML to build up {@link AccessibilityServiceInfo} objects.
     *
     * <p>
     * <strong>Note:</strong> XML parsing is a resource-heavy operation that may
     * stall, so this method should not be called while holding a lock.
     * </p>
     */
    private List<AccessibilityServiceInfo> parseAccessibilityServiceInfos(int userId) {
        List<AccessibilityServiceInfo> result = new ArrayList<>();


        int flags = PackageManager.GET_SERVICES
        int flags = PackageManager.GET_SERVICES
                | PackageManager.GET_META_DATA
                | PackageManager.GET_META_DATA
@@ -2114,12 +2188,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
                | PackageManager.MATCH_DIRECT_BOOT_AWARE
                | PackageManager.MATCH_DIRECT_BOOT_AWARE
                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;


        if (userState.getBindInstantServiceAllowedLocked()) {
        synchronized (mLock) {
            if (getUserStateLocked(userId).getBindInstantServiceAllowedLocked()) {
                flags |= PackageManager.MATCH_INSTANT;
                flags |= PackageManager.MATCH_INSTANT;
            }
            }
        }


        List<ResolveInfo> installedServices = mPackageManager.queryIntentServicesAsUser(
        List<ResolveInfo> installedServices = mPackageManager.queryIntentServicesAsUser(
                new Intent(AccessibilityService.SERVICE_INTERFACE), flags, mCurrentUserId);
                new Intent(AccessibilityService.SERVICE_INTERFACE), flags, userId);


        for (int i = 0, count = installedServices.size(); i < count; i++) {
        for (int i = 0, count = installedServices.size(); i < count; i++) {
            ResolveInfo resolveInfo = installedServices.get(i);
            ResolveInfo resolveInfo = installedServices.get(i);
@@ -2132,40 +2208,60 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
            AccessibilityServiceInfo accessibilityServiceInfo;
            AccessibilityServiceInfo accessibilityServiceInfo;
            try {
            try {
                accessibilityServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
                accessibilityServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
            } catch (XmlPullParserException | IOException xppe) {
                Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", xppe);
                continue;
            }
            if (!accessibilityServiceInfo.isWithinParcelableSize()) {
            if (!accessibilityServiceInfo.isWithinParcelableSize()) {
                Slog.e(LOG_TAG, "Skipping service "
                Slog.e(LOG_TAG, "Skipping service "
                        + accessibilityServiceInfo.getResolveInfo().getComponentInfo()
                        + accessibilityServiceInfo.getResolveInfo().getComponentInfo()
                        + " because service info size is larger than safe parcelable limits.");
                        + " because service info size is larger than safe parcelable limits.");
                continue;
                continue;
            }
            }
                if (userState.mCrashedServices.contains(serviceInfo.getComponentName())) {
            result.add(accessibilityServiceInfo);
        }
        return result;
    }

    private boolean readInstalledAccessibilityServiceLocked(AccessibilityUserState userState,
            @Nullable List<AccessibilityServiceInfo> parsedAccessibilityServiceInfos) {
        for (int i = 0, count = parsedAccessibilityServiceInfos.size(); i < count; i++) {
            AccessibilityServiceInfo accessibilityServiceInfo =
                    parsedAccessibilityServiceInfos.get(i);
            if (userState.mCrashedServices.contains(accessibilityServiceInfo.getComponentName())) {
                // Restore the crashed attribute.
                // Restore the crashed attribute.
                accessibilityServiceInfo.crashed = true;
                accessibilityServiceInfo.crashed = true;
            }
            }
                mTempAccessibilityServiceInfoList.add(accessibilityServiceInfo);
            } catch (XmlPullParserException | IOException xppe) {
                Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", xppe);
            }
        }
        }


        if (!mTempAccessibilityServiceInfoList.equals(userState.mInstalledServices)) {
        if (!parsedAccessibilityServiceInfos.equals(userState.mInstalledServices)) {
            userState.mInstalledServices.clear();
            userState.mInstalledServices.clear();
            userState.mInstalledServices.addAll(mTempAccessibilityServiceInfoList);
            userState.mInstalledServices.addAll(parsedAccessibilityServiceInfos);
            mTempAccessibilityServiceInfoList.clear();
            return true;
            return true;
        }
        }

        mTempAccessibilityServiceInfoList.clear();
        return false;
        return false;
    }
    }


    private boolean readInstalledAccessibilityShortcutLocked(AccessibilityUserState userState) {
    /**
        final List<AccessibilityShortcutInfo> shortcutInfos = AccessibilityManager
     * Returns the {@link AccessibilityShortcutInfo}s of the installed
                .getInstance(mContext).getInstalledAccessibilityShortcutListAsUser(
     * accessibility shortcut targets for the given user.
                        mContext, mCurrentUserId);
     *
        if (!shortcutInfos.equals(userState.mInstalledShortcuts)) {
     * <p>
     * <strong>Note:</strong> XML parsing is a resource-heavy operation that may
     * stall, so this method should not be called while holding a lock.
     * </p>
     */
    private List<AccessibilityShortcutInfo> parseAccessibilityShortcutInfos(int userId) {
        // TODO: b/297279151 - This should be implemented here, not by AccessibilityManager.
        return AccessibilityManager.getInstance(mContext)
                .getInstalledAccessibilityShortcutListAsUser(mContext, userId);
    }

    private boolean readInstalledAccessibilityShortcutLocked(AccessibilityUserState userState,
            List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos) {
        if (!parsedAccessibilityShortcutInfos.equals(userState.mInstalledShortcuts)) {
            userState.mInstalledShortcuts.clear();
            userState.mInstalledShortcuts.clear();
            userState.mInstalledShortcuts.addAll(shortcutInfos);
            userState.mInstalledShortcuts.addAll(parsedAccessibilityShortcutInfos);
            return true;
            return true;
        }
        }
        return false;
        return false;
@@ -2890,9 +2986,23 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
        userState.setFilterKeyEventsEnabledLocked(false);
        userState.setFilterKeyEventsEnabledLocked(false);
    }
    }


    // ErrorProne doesn't understand that this method is only called while locked,
    // returning an error for accessing mCurrentUserId.
    @SuppressWarnings("GuardedBy")
    private boolean readConfigurationForUserStateLocked(AccessibilityUserState userState) {
    private boolean readConfigurationForUserStateLocked(AccessibilityUserState userState) {
        boolean somethingChanged = readInstalledAccessibilityServiceLocked(userState);
        return readConfigurationForUserStateLocked(userState,
        somethingChanged |= readInstalledAccessibilityShortcutLocked(userState);
                parseAccessibilityServiceInfos(mCurrentUserId),
                parseAccessibilityShortcutInfos(mCurrentUserId));
    }

    private boolean readConfigurationForUserStateLocked(
            AccessibilityUserState userState,
            List<AccessibilityServiceInfo> parsedAccessibilityServiceInfos,
            List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos) {
        boolean somethingChanged = readInstalledAccessibilityServiceLocked(
                userState, parsedAccessibilityServiceInfos);
        somethingChanged |= readInstalledAccessibilityShortcutLocked(
                userState, parsedAccessibilityShortcutInfos);
        somethingChanged |= readEnabledAccessibilityServicesLocked(userState);
        somethingChanged |= readEnabledAccessibilityServicesLocked(userState);
        somethingChanged |= readTouchExplorationGrantedAccessibilityServicesLocked(userState);
        somethingChanged |= readTouchExplorationGrantedAccessibilityServicesLocked(userState);
        somethingChanged |= readTouchExplorationEnabledSettingLocked(userState);
        somethingChanged |= readTouchExplorationEnabledSettingLocked(userState);
+85 −0
Original line number Original line Diff line number Diff line
@@ -33,6 +33,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mock;
@@ -53,12 +54,18 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.ServiceInfo;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Icon;
import android.graphics.drawable.Icon;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.DisplayManagerGlobal;
import android.net.Uri;
import android.os.Bundle;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IBinder;
import android.os.LocaleList;
import android.os.LocaleList;
import android.os.UserHandle;
import android.os.UserHandle;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
import android.provider.Settings;
import android.testing.TestableContext;
import android.testing.TestableContext;
import android.view.Display;
import android.view.Display;
@@ -93,8 +100,13 @@ import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;


import java.util.ArrayList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;


/**
/**
 * APCT tests for {@link AccessibilityManagerService}.
 * APCT tests for {@link AccessibilityManagerService}.
@@ -104,6 +116,10 @@ public class AccessibilityManagerServiceTest {
    public final A11yTestableContext mTestableContext = new A11yTestableContext(
    public final A11yTestableContext mTestableContext = new A11yTestableContext(
            ApplicationProvider.getApplicationContext());
            ApplicationProvider.getApplicationContext());


    @Rule
    public final CheckFlagsRule mCheckFlagsRule =
            DeviceFlagsValueProvider.createCheckFlagsRule();

    private static final int ACTION_ID = 20;
    private static final int ACTION_ID = 20;
    private static final String LABEL = "label";
    private static final String LABEL = "label";
    private static final String INTENT_ACTION = "TESTACTION";
    private static final String INTENT_ACTION = "TESTACTION";
@@ -204,6 +220,8 @@ public class AccessibilityManagerServiceTest {
                mA11yms.getCurrentUserIdLocked());
                mA11yms.getCurrentUserIdLocked());
        when(mMockServiceInfo.getResolveInfo()).thenReturn(mMockResolveInfo);
        when(mMockServiceInfo.getResolveInfo()).thenReturn(mMockResolveInfo);
        mMockResolveInfo.serviceInfo = mock(ServiceInfo.class);
        mMockResolveInfo.serviceInfo = mock(ServiceInfo.class);
        mMockResolveInfo.serviceInfo.packageName = "packageName";
        mMockResolveInfo.serviceInfo.name = "className";
        mMockResolveInfo.serviceInfo.applicationInfo = mock(ApplicationInfo.class);
        mMockResolveInfo.serviceInfo.applicationInfo = mock(ApplicationInfo.class);


        when(mMockBinder.queryLocalInterface(any())).thenReturn(mMockServiceClient);
        when(mMockBinder.queryLocalInterface(any())).thenReturn(mMockServiceClient);
@@ -581,6 +599,73 @@ public class AccessibilityManagerServiceTest {
                ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString());
                ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString());
    }
    }


    @Test
    @RequiresFlagsDisabled(Flags.FLAG_SCAN_PACKAGES_WITHOUT_LOCK)
    // Test old behavior to validate lock detection for the old (locked access) case.
    public void testPackageMonitorScanPackages_scansWhileHoldingLock() {
        setupAccessibilityServiceConnection(0);
        final AtomicReference<Set<Boolean>> lockState = collectLockStateWhilePackageScanning();
        when(mMockPackageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt()))
                .thenReturn(List.of(mMockResolveInfo));
        when(mMockSecurityPolicy.canRegisterService(any())).thenReturn(true);

        final Intent packageIntent = new Intent(Intent.ACTION_PACKAGE_ADDED);
        packageIntent.setData(Uri.parse("test://package"));
        packageIntent.putExtra(Intent.EXTRA_USER_HANDLE, mA11yms.getCurrentUserIdLocked());
        packageIntent.putExtra(Intent.EXTRA_REPLACING, true);
        mA11yms.getPackageMonitor().doHandlePackageEvent(packageIntent);

        assertThat(lockState.get()).containsExactly(true);
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_SCAN_PACKAGES_WITHOUT_LOCK)
    public void testPackageMonitorScanPackages_scansWithoutHoldingLock() {
        setupAccessibilityServiceConnection(0);
        final AtomicReference<Set<Boolean>> lockState = collectLockStateWhilePackageScanning();
        when(mMockPackageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt()))
                .thenReturn(List.of(mMockResolveInfo));
        when(mMockSecurityPolicy.canRegisterService(any())).thenReturn(true);

        final Intent packageIntent = new Intent(Intent.ACTION_PACKAGE_ADDED);
        packageIntent.setData(Uri.parse("test://package"));
        packageIntent.putExtra(Intent.EXTRA_USER_HANDLE, mA11yms.getCurrentUserIdLocked());
        packageIntent.putExtra(Intent.EXTRA_REPLACING, true);
        mA11yms.getPackageMonitor().doHandlePackageEvent(packageIntent);

        assertThat(lockState.get()).containsExactly(false);
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_SCAN_PACKAGES_WITHOUT_LOCK)
    public void testSwitchUserScanPackages_scansWithoutHoldingLock() {
        setupAccessibilityServiceConnection(0);
        final AtomicReference<Set<Boolean>> lockState = collectLockStateWhilePackageScanning();
        when(mMockPackageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt()))
                .thenReturn(List.of(mMockResolveInfo));
        when(mMockSecurityPolicy.canRegisterService(any())).thenReturn(true);

        mA11yms.switchUser(mA11yms.getCurrentUserIdLocked() + 1);

        assertThat(lockState.get()).containsExactly(false);
    }

    // Single package intents can trigger multiple PackageMonitor callbacks.
    // Collect the state of the lock in a set, since tests only care if calls
    // were all locked or all unlocked.
    private AtomicReference<Set<Boolean>> collectLockStateWhilePackageScanning() {
        final AtomicReference<Set<Boolean>> lockState =
                new AtomicReference<>(new HashSet<Boolean>());
        doAnswer((Answer<XmlResourceParser>) invocation -> {
            lockState.updateAndGet(set -> {
                set.add(mA11yms.unsafeIsLockHeld());
                return set;
            });
            return null;
        }).when(mMockResolveInfo.serviceInfo).loadXmlMetaData(any(), any());
        return lockState;
    }

    private void mockManageAccessibilityGranted(TestableContext context) {
    private void mockManageAccessibilityGranted(TestableContext context) {
        context.getTestablePermissions().setPermission(Manifest.permission.MANAGE_ACCESSIBILITY,
        context.getTestablePermissions().setPermission(Manifest.permission.MANAGE_ACCESSIBILITY,
                PackageManager.PERMISSION_GRANTED);
                PackageManager.PERMISSION_GRANTED);