summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Felipe Leme <felipeal@google.com> 2023-03-20 08:40:43 -0700
committer Felipe Leme <felipeal@google.com> 2023-04-11 16:57:39 -0700
commitea241d52ed0bcf08253a3d9f8c137fa6ab77e08b (patch)
tree8bf2c0cab5b76e3077695b1525ab002c086447f2
parent2255f88021588b812b8e6943a91a0847ca0a6c7c (diff)
Fix UiAutomation for visible background users.
UiAutomation is "implemented" as an accessibility service, and the A11Y infra doesn't support concurrent users (i.e., it only supports the current user and its profiles). This CL works around that limitation by temporarily setting the current user as the visible background user while the UiAutomation connection is set. Test: atest CtsUiAutomationTestCases android.app.cts.InstrumentationTest # CTS Test: atest --user-type secondary_user_on_secondary_display CtsUiAutomationTestCases # CTS Test: atest FrameworksCoreTests:android.app.InstrumentationTest FrameworksCoreTests:android.app.UiAutomationTest AccessibilityManagerServiceTest AccessibilitySecurityPolicyTest AccessibilityWindowManagerTest # unit Fixes: 271188189 Change-Id: I8603e09e0bc8ee2aebb43dfe28dd3531cda05c37
-rw-r--r--core/java/android/app/Instrumentation.java3
-rw-r--r--core/java/android/app/UiAutomation.java132
-rw-r--r--core/java/android/app/UiAutomationConnection.java11
-rw-r--r--core/java/android/view/accessibility/IAccessibilityManager.aidl2
-rw-r--r--core/tests/coretests/src/android/app/UiAutomationTest.java144
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java122
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilitySecurityPolicy.java7
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java38
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);