diff options
8 files changed, 432 insertions, 27 deletions
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index c131ce574d2c..e31486f18dbf 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -2354,8 +2354,7 @@ public class Instrumentation { return mUiAutomation; } if (mustCreateNewAutomation) { - mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(), - mUiAutomationConnection); + mUiAutomation = new UiAutomation(getTargetContext(), mUiAutomationConnection); } else { mUiAutomation.disconnect(); } diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 658e08444006..247d5bc77ffb 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -16,6 +16,8 @@ package android.app; +import static android.view.Display.DEFAULT_DISPLAY; + import android.accessibilityservice.AccessibilityGestureEvent; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityService.Callbacks; @@ -30,6 +32,7 @@ import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; @@ -45,6 +48,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.util.ArraySet; import android.util.DebugUtils; import android.util.Log; @@ -69,8 +73,10 @@ import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.inputmethod.EditorInfo; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback; import com.android.internal.inputmethod.RemoteAccessibilityInputConnection; +import com.android.internal.util.Preconditions; import com.android.internal.util.function.pooled.PooledLambda; import libcore.io.IoUtils; @@ -202,6 +208,8 @@ public final class UiAutomation { private final IUiAutomationConnection mUiAutomationConnection; + private final int mDisplayId; + private HandlerThread mRemoteCallbackThread; private IAccessibilityServiceClient mClient; @@ -261,24 +269,49 @@ public final class UiAutomation { /** * Creates a new instance that will handle callbacks from the accessibility + * layer on the thread of the provided context main looper and perform requests for privileged + * operations on the provided connection, and filtering display-related features to the display + * associated with the context (or the user running the test, on devices that + * {@link UserManager#isVisibleBackgroundUsersSupported() support visible background users}). + * + * @param context the context associated with the automation + * @param connection The connection for performing privileged operations. + * + * @hide + */ + public UiAutomation(Context context, IUiAutomationConnection connection) { + this(getDisplayId(context), context.getMainLooper(), connection); + } + + /** + * Creates a new instance that will handle callbacks from the accessibility * layer on the thread of the provided looper and perform requests for privileged * operations on the provided connection. * * @param looper The looper on which to execute accessibility callbacks. * @param connection The connection for performing privileged operations. * + * @deprecated use {@link #UiAutomation(Context, IUiAutomationConnection)} instead + * * @hide */ + @Deprecated @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public UiAutomation(Looper looper, IUiAutomationConnection connection) { - if (looper == null) { - throw new IllegalArgumentException("Looper cannot be null!"); - } - if (connection == null) { - throw new IllegalArgumentException("Connection cannot be null!"); - } + this(DEFAULT_DISPLAY, looper, connection); + Log.w(LOG_TAG, "Created with deprecatead constructor, assumes DEFAULT_DISPLAY"); + } + + private UiAutomation(int displayId, Looper looper, IUiAutomationConnection connection) { + Preconditions.checkArgument(looper != null, "Looper cannot be null!"); + Preconditions.checkArgument(connection != null, "Connection cannot be null!"); + mLocalCallbackHandler = new Handler(looper); mUiAutomationConnection = connection; + mDisplayId = displayId; + + Log.i(LOG_TAG, "Initialized for user " + Process.myUserHandle().getIdentifier() + + " on display " + mDisplayId); } /** @@ -719,8 +752,14 @@ public final class UiAutomation { } /** - * Gets the windows on the screen of the default display. This method returns only the windows - * that a sighted user can interact with, as opposed to all windows. + * Gets the windows on the screen associated with the {@link UiAutomation} context (usually the + * {@link android.view.Display#DEFAULT_DISPLAY default display). + * + * <p> + * This method returns only the windows that a sighted user can interact with, as opposed to + * all windows. + + * <p> * For example, if there is a modal dialog shown and the user cannot touch * anything behind it, then only the modal window will be reported * (assuming it is the top one). For convenience the returned windows @@ -730,21 +769,23 @@ public final class UiAutomation { * <strong>Note:</strong> In order to access the windows you have to opt-in * to retrieve the interactive windows by setting the * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag. - * </p> * * @return The windows if there are windows such, otherwise an empty list. * @throws IllegalStateException If the connection to the accessibility subsystem is not * established. */ public List<AccessibilityWindowInfo> getWindows() { + if (DEBUG) { + Log.d(LOG_TAG, "getWindows(): returning windows for display " + mDisplayId); + } final int connectionId; synchronized (mLock) { throwIfNotConnectedLocked(); connectionId = mConnectionId; } // Calling out without a lock held. - return AccessibilityInteractionClient.getInstance() - .getWindows(connectionId); + return AccessibilityInteractionClient.getInstance().getWindowsOnDisplay(connectionId, + mDisplayId); } /** @@ -1112,8 +1153,10 @@ public final class UiAutomation { * @return The screenshot bitmap on success, null otherwise. */ public Bitmap takeScreenshot() { - Display display = DisplayManagerGlobal.getInstance() - .getRealDisplay(Display.DEFAULT_DISPLAY); + if (DEBUG) { + Log.d(LOG_TAG, "Taking screenshot of display " + mDisplayId); + } + Display display = DisplayManagerGlobal.getInstance().getRealDisplay(mDisplayId); Point displaySize = new Point(); display.getRealSize(displaySize); @@ -1126,10 +1169,12 @@ public final class UiAutomation { screenShot = mUiAutomationConnection.takeScreenshot( new Rect(0, 0, displaySize.x, displaySize.y)); if (screenShot == null) { + Log.e(LOG_TAG, "mUiAutomationConnection.takeScreenshot() returned null for display " + + mDisplayId); return null; } } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while taking screenshot!", re); + Log.e(LOG_TAG, "Error while taking screenshot of display " + mDisplayId, re); return null; } @@ -1509,6 +1554,14 @@ public final class UiAutomation { return executeShellCommandInternal(command, true /* includeStderr */); } + /** + * @hide + */ + @VisibleForTesting + public int getDisplayId() { + return mDisplayId; + } + private ParcelFileDescriptor[] executeShellCommandInternal( String command, boolean includeStderr) { warnIfBetterCommand(command); @@ -1564,6 +1617,7 @@ public final class UiAutomation { final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("UiAutomation@").append(Integer.toHexString(hashCode())); stringBuilder.append("[id=").append(mConnectionId); + stringBuilder.append(", displayId=").append(mDisplayId); stringBuilder.append(", flags=").append(mFlags); stringBuilder.append("]"); return stringBuilder.toString(); @@ -1601,6 +1655,55 @@ public final class UiAutomation { return (mFlags & UiAutomation.FLAG_DONT_USE_ACCESSIBILITY) == 0; } + /** + * Gets the display id associated with the UiAutomation context. + * + * <p><b>NOTE: </b> must be a static method because it's called from a constructor to call + * another one. + */ + private static int getDisplayId(Context context) { + Preconditions.checkArgument(context != null, "Context cannot be null!"); + + UserManager userManager = context.getSystemService(UserManager.class); + // TODO(b/255426725): given that this is a temporary solution until a11y supports multiple + // users, the display is only set on devices that support that + if (!userManager.isVisibleBackgroundUsersSupported()) { + return DEFAULT_DISPLAY; + } + + int displayId = context.getDisplayId(); + if (displayId == Display.INVALID_DISPLAY) { + // Shouldn't happen, but we better handle it + Log.e(LOG_TAG, "UiAutomation created UI context with invalid display id, assuming it's" + + " running in the display assigned to the user"); + return getMainDisplayIdAssignedToUser(context, userManager); + } + + if (displayId != DEFAULT_DISPLAY) { + if (DEBUG) { + Log.d(LOG_TAG, "getDisplayId(): returning context's display (" + displayId + ")"); + } + // Context is explicitly setting the display, so we respect that... + return displayId; + } + // ...otherwise, we need to get the display the test's user is running on + int userDisplayId = getMainDisplayIdAssignedToUser(context, userManager); + if (DEBUG) { + Log.d(LOG_TAG, "getDisplayId(): returning user's display (" + userDisplayId + ")"); + } + return userDisplayId; + } + + private static int getMainDisplayIdAssignedToUser(Context context, UserManager userManager) { + if (!userManager.isUserVisible()) { + // Should also not happen, but ... + Log.e(LOG_TAG, "User (" + context.getUserId() + ") is not visible, using " + + "DEFAULT_DISPLAY"); + return DEFAULT_DISPLAY; + } + return userManager.getMainDisplayIdAssignedToUser(); + } + private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper { public IAccessibilityServiceClientImpl(Looper looper, int generationId) { @@ -1621,6 +1724,7 @@ public final class UiAutomation { if (DEBUG) { Log.d(LOG_TAG, "init(): connectionId=" + connectionId + ", windowToken=" + windowToken + ", user=" + Process.myUserHandle() + + ", UiAutomation.mDisplay=" + UiAutomation.this.mDisplayId + ", mGenerationId=" + mGenerationId + ", UiAutomation.mGenerationId=" + UiAutomation.this.mGenerationId); diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java index 13e800e38cca..d96a9d104ec2 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -22,6 +22,7 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceClient; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.Bitmap; @@ -117,7 +118,8 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { throw new IllegalStateException("Already connected."); } mOwningUid = Binder.getCallingUid(); - registerUiTestAutomationServiceLocked(client, flags); + registerUiTestAutomationServiceLocked(client, + Binder.getCallingUserHandle().getIdentifier(), flags); storeRotationStateLocked(); } } @@ -553,7 +555,7 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client, - int flags) { + @UserIdInt int userId, int flags) { IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface( ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)); final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); @@ -571,10 +573,11 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { try { // Calling out with a lock held is fine since if the system // process is gone the client calling in will be killed. - manager.registerUiTestAutomationService(mToken, client, info, flags); + manager.registerUiTestAutomationService(mToken, client, info, userId, flags); mClient = client; } catch (RemoteException re) { - throw new IllegalStateException("Error while registering UiTestAutomationService.", re); + throw new IllegalStateException("Error while registering UiTestAutomationService for " + + "user " + userId + ".", re); } } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 1fac142df481..390503b80bad 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -62,7 +62,7 @@ interface IAccessibilityManager { in IAccessibilityInteractionConnection connection); void registerUiTestAutomationService(IBinder owner, IAccessibilityServiceClient client, - in AccessibilityServiceInfo info, int flags); + in AccessibilityServiceInfo info, int userId, int flags); void unregisterUiTestAutomationService(IAccessibilityServiceClient client); diff --git a/core/tests/coretests/src/android/app/UiAutomationTest.java b/core/tests/coretests/src/android/app/UiAutomationTest.java new file mode 100644 index 000000000000..3ac5057bcf24 --- /dev/null +++ b/core/tests/coretests/src/android/app/UiAutomationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 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 android.app; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Looper; +import android.os.UserManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public final class UiAutomationTest { + + private static final int SECONDARY_DISPLAY_ID = 42; + private static final int DISPLAY_ID_ASSIGNED_TO_USER = 108; + + private final Looper mLooper = Looper.getMainLooper(); + + @Mock + private Context mContext; + @Mock + private UserManager mUserManager; + @Mock + private IUiAutomationConnection mConnection; + + @Before + public void setFixtures() { + when(mContext.getMainLooper()).thenReturn(mLooper); + mockSystemService(UserManager.class, mUserManager); + + // Set default expectations + mockVisibleBackgroundUsersSupported(/* supported= */ false); + mockUserVisibility(/* visible= */ true); + // make sure it's not used, unless explicitly mocked + mockDisplayAssignedToUser(INVALID_DISPLAY); + mockContextDisplay(DEFAULT_DISPLAY); + } + + @Test + public void testContextConstructor_nullContext() { + assertThrows(IllegalArgumentException.class, + () -> new UiAutomation((Context) null, mConnection)); + } + + @Test + public void testContextConstructor_nullConnection() { + assertThrows(IllegalArgumentException.class, + () -> new UiAutomation(mContext, (IUiAutomationConnection) null)); + } + + @Test + public void testGetDisplay_contextWithSecondaryDisplayId() { + mockContextDisplay(SECONDARY_DISPLAY_ID); + + UiAutomation uiAutomation = new UiAutomation(mContext, mConnection); + + // It's always DEFAULT_DISPLAY regardless, unless the device supports visible bg users + assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId()) + .isEqualTo(DEFAULT_DISPLAY); + } + + @Test + public void testGetDisplay_contextWithInvalidDisplayId() { + mockContextDisplay(INVALID_DISPLAY); + + UiAutomation uiAutomation = new UiAutomation(mContext, mConnection); + + assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId()) + .isEqualTo(DEFAULT_DISPLAY); + } + + @Test + public void testGetDisplay_visibleBgUsers() { + mockVisibleBackgroundUsersSupported(/* supported= */ true); + mockContextDisplay(SECONDARY_DISPLAY_ID); + // Should be using display from context, not from user + mockDisplayAssignedToUser(DISPLAY_ID_ASSIGNED_TO_USER); + + UiAutomation uiAutomation = new UiAutomation(mContext, mConnection); + + assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId()) + .isEqualTo(SECONDARY_DISPLAY_ID); + } + + @Test + public void testGetDisplay_visibleBgUsers_contextWithInvalidDisplayId() { + mockVisibleBackgroundUsersSupported(/* supported= */ true); + mockContextDisplay(INVALID_DISPLAY); + mockDisplayAssignedToUser(DISPLAY_ID_ASSIGNED_TO_USER); + + UiAutomation uiAutomation = new UiAutomation(mContext, mConnection); + + assertWithMessage("getDisplayId()").that(uiAutomation.getDisplayId()) + .isEqualTo(DISPLAY_ID_ASSIGNED_TO_USER); + } + + private <T> void mockSystemService(Class<T> svcClass, T svc) { + String svcName = svcClass.getName(); + when(mContext.getSystemServiceName(svcClass)).thenReturn(svcName); + when(mContext.getSystemService(svcName)).thenReturn(svc); + } + + private void mockVisibleBackgroundUsersSupported(boolean supported) { + when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(supported); + } + + private void mockContextDisplay(int displayId) { + when(mContext.getDisplayId()).thenReturn(displayId); + } + + private void mockDisplayAssignedToUser(int displayId) { + when(mUserManager.getMainDisplayIdAssignedToUser()).thenReturn(displayId); + } + + private void mockUserVisibility(boolean visible) { + when(mUserManager.isUserVisible()).thenReturn(visible); + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 51325e72204d..0bdb0c80d219 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -112,6 +112,7 @@ import android.util.IntArray; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.Display; import android.view.IWindow; import android.view.InputDevice; @@ -160,6 +161,7 @@ import com.android.server.accessibility.magnification.WindowMagnificationManager import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.WindowManagerPolicy; +import com.android.server.utils.Slogf; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; import com.android.settingslib.RestrictedLockUtils; @@ -301,7 +303,23 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final List<SendWindowStateChangedEventRunnable> mSendWindowStateChangedEventRunnables = new ArrayList<>(); - private int mCurrentUserId = UserHandle.USER_SYSTEM; + @GuardedBy("mLock") + private @UserIdInt int mCurrentUserId = UserHandle.USER_SYSTEM; + + // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation: + // when the UiAutomation is set in a visible background user, mCurrentUserId points to that user + // and mRealCurrentUserId points to the "real" current user; otherwise, mRealCurrentUserId + // is set as UserHandle.USER_CURRENT. + @GuardedBy("mLock") + private @UserIdInt int mRealCurrentUserId = UserHandle.USER_CURRENT; + + // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation + // purposes - in the long term, the whole service should be refactored so it handles "visible" + // users, not current user. Notice that because this is temporary, it's not trying to optimize + // performance / utilization (for example, it's not using an IntArray) + @GuardedBy("mLock") + @Nullable // only set when device supports visible background users + private final SparseBooleanArray mVisibleBgUserIds; //TODO: Remove this hack private boolean mInitialized; @@ -316,6 +334,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private SparseArray<SurfaceControl> mA11yOverlayLayers = new SparseArray<>(); private final FlashNotificationsController mFlashNotificationsController; + private final UserManagerInternal mUmi; private AccessibilityUserState getCurrentUserStateLocked() { return getUserStateLocked(mCurrentUserId); @@ -445,6 +464,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mHasInputFilter = true; } mFlashNotificationsController = new FlashNotificationsController(mContext); + mUmi = LocalServices.getService(UserManagerInternal.class); + // TODO(b/255426725): not used on tests + mVisibleBgUserIds = null; + init(); } @@ -477,6 +500,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mProxyManager = new ProxyManager(mLock, mA11yWindowManager, mContext, mMainHandler, mUiAutomationManager, this); mFlashNotificationsController = new FlashNotificationsController(mContext); + mUmi = LocalServices.getService(UserManagerInternal.class); + + if (UserManager.isVisibleBackgroundUsersEnabled()) { + mVisibleBgUserIds = new SparseBooleanArray(); + mUmi.addUserVisibilityListener((u, v) -> onUserVisibilityChanged(u, v)); + } else { + mVisibleBgUserIds = null; + } + init(); } @@ -493,6 +525,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return mCurrentUserId; } + @GuardedBy("mLock") + @Override + public SparseBooleanArray getVisibleUserIdsLocked() { + return mVisibleBgUserIds; + } + @Override public boolean isAccessibilityButtonShown() { return mIsAccessibilityButtonShown; @@ -1362,6 +1400,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public void registerUiTestAutomationService(IBinder owner, IAccessibilityServiceClient serviceClient, AccessibilityServiceInfo accessibilityServiceInfo, + int userId, int flags) { if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) { mTraceManager.logTrace(LOG_TAG + ".registerUiTestAutomationService", @@ -1374,6 +1413,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE); synchronized (mLock) { + changeCurrentUserForTestAutomationIfNeededLocked(userId); mUiAutomationManager.registerUiTestAutomationServiceLocked(owner, serviceClient, mContext, accessibilityServiceInfo, sIdCounter++, mMainHandler, mSecurityPolicy, this, getTraceManager(), mWindowManagerService, @@ -1390,7 +1430,47 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } synchronized (mLock) { mUiAutomationManager.unregisterUiTestAutomationServiceLocked(serviceClient); + restoreCurrentUserAfterTestAutomationIfNeededLocked(); + } + } + + // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation + @GuardedBy("mLock") + private void changeCurrentUserForTestAutomationIfNeededLocked(@UserIdInt int userId) { + if (mVisibleBgUserIds == null) { + Slogf.d(LOG_TAG, "changeCurrentUserForTestAutomationIfNeededLocked(%d): ignoring " + + "because device doesn't support visible background users", userId); + return; + } + if (!mVisibleBgUserIds.get(userId)) { + Slogf.wtf(LOG_TAG, "Cannot change current user to %d as it's not visible " + + "(mVisibleUsers=%s)", userId, mVisibleBgUserIds); + return; + } + if (mCurrentUserId == userId) { + Slogf.w(LOG_TAG, "NOT changing current user for test automation purposes as it is " + + "already %d", mCurrentUserId); + return; + } + Slogf.i(LOG_TAG, "Changing current user from %d to %d for test automation purposes", + mCurrentUserId, userId); + mRealCurrentUserId = mCurrentUserId; + switchUser(userId); + } + + // TODO(b/255426725): temporary workaround to support visible background users for UiAutomation + @GuardedBy("mLock") + private void restoreCurrentUserAfterTestAutomationIfNeededLocked() { + if (mVisibleBgUserIds == null) { + Slogf.d(LOG_TAG, "restoreCurrentUserForTestAutomationIfNeededLocked(): ignoring " + + "because device doesn't support visible background users"); + return; } + Slogf.i(LOG_TAG, "Restoring current user to %d after using %d for test automation purposes", + mRealCurrentUserId, mCurrentUserId); + int currentUserId = mRealCurrentUserId; + mRealCurrentUserId = UserHandle.USER_CURRENT; + switchUser(currentUserId); } @Override @@ -2291,8 +2371,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private void updateServicesLocked(AccessibilityUserState userState) { Map<ComponentName, AccessibilityServiceConnection> componentNameToServiceMap = userState.mComponentNameToServiceMap; - boolean isUnlockingOrUnlocked = LocalServices.getService(UserManagerInternal.class) - .isUserUnlockingOrUnlocked(userState.mUserId); + boolean isUnlockingOrUnlocked = mUmi.isUserUnlockingOrUnlocked(userState.mUserId); for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) { AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i); @@ -2593,6 +2672,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + private void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) { + if (DEBUG) { + Slogf.d(LOG_TAG, "onUserVisibilityChanged(): %d => %b", userId, visible); + } + synchronized (mLock) { + if (visible) { + mVisibleBgUserIds.put(userId, visible); + } else { + mVisibleBgUserIds.delete(userId); + } + } + } + /** * Called when any property of the user state has changed. * @@ -4025,7 +4117,16 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub pw.println("ACCESSIBILITY MANAGER (dumpsys accessibility)"); pw.println(); pw.append("currentUserId=").append(String.valueOf(mCurrentUserId)); + if (mRealCurrentUserId != UserHandle.USER_CURRENT + && mCurrentUserId != mRealCurrentUserId) { + pw.append(" (set for UiAutomation purposes; \"real\" current user is ") + .append(String.valueOf(mRealCurrentUserId)).append(")"); + } pw.println(); + if (mVisibleBgUserIds != null) { + pw.append("visibleBgUserIds=").append(mVisibleBgUserIds.toString()); + pw.println(); + } pw.append("hasWindowMagnificationConnection=").append( String.valueOf(getWindowMagnificationMgr().isConnected())); pw.println(); @@ -4052,6 +4153,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } pw.println(); mProxyManager.dump(fd, pw, args); + mA11yDisplayListener.dump(fd, pw, args); } } @@ -4437,6 +4539,20 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub /* do nothing */ } + void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("Accessibility Display Listener:"); + pw.println(" SystemUI uid: " + mSystemUiUid); + int size = mDisplaysList.size(); + pw.printf(" %d valid display%s: ", size, (size == 1 ? "" : "s")); + for (int i = 0; i < size; i++) { + pw.print(mDisplaysList.get(i).getDisplayId()); + if (i < size - 1) { + pw.print(", "); + } + } + pw.println(); + } + private boolean isValidDisplay(@Nullable Display display) { if (display == null || display.getType() == Display.TYPE_OVERLAY) { return false; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java index c37ea501bbc9..88656239e59b 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java @@ -39,6 +39,7 @@ 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; @@ -88,6 +89,12 @@ public class AccessibilitySecurityPolicy { */ 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; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java index baed181ebd43..a8a536590004 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java @@ -51,6 +51,7 @@ import android.view.accessibility.IAccessibilityInteractionConnection; import com.android.internal.annotations.VisibleForTesting; import com.android.server.accessibility.AccessibilitySecurityPolicy.AccessibilityUserManager; +import com.android.server.utils.Slogf; import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; @@ -59,6 +60,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * This class provides APIs for accessibility manager to manage {@link AccessibilityWindowInfo}s and @@ -67,6 +69,7 @@ import java.util.List; public class AccessibilityWindowManager { private static final String LOG_TAG = "AccessibilityWindowManager"; private static final boolean DEBUG = false; + private static final boolean VERBOSE = false; private static int sNextWindowId; @@ -209,6 +212,9 @@ public class AccessibilityWindowManager { * Constructor for DisplayWindowsObserver. */ DisplayWindowsObserver(int displayId) { + if (DEBUG) { + Slogf.d(LOG_TAG, "Creating DisplayWindowsObserver for displayId %d", displayId); + } mDisplayId = displayId; } @@ -430,12 +436,27 @@ public class AccessibilityWindowManager { synchronized (mLock) { updateWindowsByWindowAttributesLocked(windows); if (DEBUG) { - Slog.i(LOG_TAG, "Display Id = " + mDisplayId); - Slog.i(LOG_TAG, "Windows changed: " + windows); + Slogf.i(LOG_TAG, "mDisplayId=%d, topFocusedDisplayId=%d, currentUserId=%d, " + + "visibleBgUsers=%s", mDisplayId, topFocusedDisplayId, + mAccessibilityUserManager.getCurrentUserIdLocked(), + mAccessibilityUserManager.getVisibleUserIdsLocked()); + if (VERBOSE) { + Slogf.i(LOG_TAG, "%d windows changed: %s ", windows.size(), windows); + } else { + List<String> windowsInfo = windows.stream() + .map(w -> "{displayId=" + w.displayId + ", title=" + w.title + "}") + .collect(Collectors.toList()); + Slogf.i(LOG_TAG, "%d windows changed: %s", windows.size(), windowsInfo); + } } if (shouldUpdateWindowsLocked(forceSend, windows)) { mTopFocusedDisplayId = topFocusedDisplayId; mTopFocusedWindowToken = topFocusedWindowToken; + if (DEBUG) { + Slogf.d(LOG_TAG, "onWindowsForAccessibilityChanged(): updating windows for " + + "display %d and token %s", + topFocusedDisplayId, topFocusedWindowToken); + } cacheWindows(windows); // Lets the policy update the focused and active windows. updateWindowsLocked(mAccessibilityUserManager.getCurrentUserIdLocked(), @@ -443,6 +464,11 @@ public class AccessibilityWindowManager { // Someone may be waiting for the windows - advertise it. mLock.notifyAll(); } + else if (DEBUG) { + Slogf.d(LOG_TAG, "onWindowsForAccessibilityChanged(): NOT updating windows for " + + "display %d and token %s", + topFocusedDisplayId, topFocusedWindowToken); + } } } @@ -472,6 +498,12 @@ public class AccessibilityWindowManager { } final int windowCount = windows.size(); + if (VERBOSE) { + Slogf.v(LOG_TAG, + "shouldUpdateWindowsLocked(): mDisplayId=%d, windowCount=%d, " + + "mCachedWindowInfos.size()=%d, windows.size()=%d", mDisplayId, + windowCount, mCachedWindowInfos.size(), windows.size()); + } // We computed the windows and if they changed notify the client. if (mCachedWindowInfos.size() != windowCount) { // Different size means something changed. @@ -1274,7 +1306,7 @@ public class AccessibilityWindowManager { */ @Nullable public RemoteAccessibilityConnection getConnectionLocked(int userId, int windowId) { - if (DEBUG) { + if (VERBOSE) { Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId); } RemoteAccessibilityConnection connection = mGlobalInteractionConnections.get(windowId); |