Force input focus to the last focused non-proxy display.
This only happens under a couple of conditions:
1) An A11yService is about to perform a system action.
2) An A11yService has touch exploration enabled, and
TouchState has received a TYPE_TOUCH_INTERACTION_END
AccessibilityEvent, meaning the user has just completed a touch
interaction.
In both cases, the top focus will only move if the current top focused
display is being proxyed, i.e. the display is being streamed to a
ChromeBook with a11y enabled.
Keep the places where focus changes minimal, since ideally a
VirtualDeviceManager client like Exo should not be stealing focus and
this should be fixed on the input level.
In the fallback case where there is no register system action so we
directly send key events, target the last focused non-proxy display.
Bug: 274511647
Test: atest AccessibilityDisplayProxyTest and manual (see videos in
comments)
Change-Id: I79f5b4806eeed4fd222b811a919ddb296e83a895
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 51325e7..02bd89e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -192,7 +192,8 @@
AccessibilityUserState.ServiceInfoChangeListener,
AccessibilityWindowManager.AccessibilityEventSender,
AccessibilitySecurityPolicy.AccessibilityUserManager,
- SystemActionPerformer.SystemActionsChangedListener, ProxyManager.SystemSupport{
+ SystemActionPerformer.SystemActionsChangedListener,
+ SystemActionPerformer.DisplayUpdateCallBack, ProxyManager.SystemSupport {
private static final boolean DEBUG = false;
@@ -1181,7 +1182,7 @@
private SystemActionPerformer getSystemActionPerformer() {
if (mSystemActionPerformer == null) {
mSystemActionPerformer =
- new SystemActionPerformer(mContext, mWindowManagerService, null, this);
+ new SystemActionPerformer(mContext, mWindowManagerService, null, this, this);
}
return mSystemActionPerformer;
}
@@ -1531,6 +1532,18 @@
}
}
+ @Override
+ // TODO(b/276459590): Remove when this is resolved at the virtual device/input level.
+ public void moveNonProxyTopFocusedDisplayToTopIfNeeded() {
+ mA11yWindowManager.moveNonProxyTopFocusedDisplayToTopIfNeeded();
+ }
+
+ @Override
+ // TODO(b/276459590): Remove when this is resolved at the virtual device/input level.
+ public int getLastNonProxyTopFocusedDisplayId() {
+ return mA11yWindowManager.getLastNonProxyTopFocusedDisplayId();
+ }
+
@VisibleForTesting
void notifySystemActionsChangedLocked(AccessibilityUserState userState) {
for (int i = userState.mBoundServices.size() - 1; i >= 0; i--) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index 0e25a06..c9ebf4c 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -100,6 +100,9 @@
// The top focused display and window token updated with the callback of window lists change.
private int mTopFocusedDisplayId;
private IBinder mTopFocusedWindowToken;
+
+ // The non-proxy display that most recently had top focus.
+ private int mLastNonProxyTopFocusedDisplayId;
// The display has the accessibility focused window currently.
private int mAccessibilityFocusedDisplayId = Display.INVALID_DISPLAY;
@@ -434,6 +437,9 @@
}
if (shouldUpdateWindowsLocked(forceSend, windows)) {
mTopFocusedDisplayId = topFocusedDisplayId;
+ if (!isProxyed(topFocusedDisplayId)) {
+ mLastNonProxyTopFocusedDisplayId = topFocusedDisplayId;
+ }
mTopFocusedWindowToken = topFocusedWindowToken;
cacheWindows(windows);
// Lets the policy update the focused and active windows.
@@ -1108,6 +1114,21 @@
return false;
}
+ private boolean isProxyed(int displayId) {
+ final DisplayWindowsObserver observer = mDisplayWindowsObservers.get(displayId);
+ return (observer != null && observer.mIsProxy);
+ }
+
+ void moveNonProxyTopFocusedDisplayToTopIfNeeded() {
+ if (mHasProxy
+ && (mLastNonProxyTopFocusedDisplayId != mTopFocusedDisplayId)) {
+ mWindowManagerInternal.moveDisplayToTopIfAllowed(mLastNonProxyTopFocusedDisplayId);
+ }
+ }
+ int getLastNonProxyTopFocusedDisplayId() {
+ return mLastNonProxyTopFocusedDisplayId;
+ }
+
/**
* Checks if we are tracking windows on specified display.
*
diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
index c89b9b8..a13df47 100644
--- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
@@ -72,6 +72,13 @@
}
private final SystemActionsChangedListener mListener;
+ interface DisplayUpdateCallBack {
+ void moveNonProxyTopFocusedDisplayToTopIfNeeded();
+
+ int getLastNonProxyTopFocusedDisplayId();
+ }
+ private final DisplayUpdateCallBack mDisplayUpdateCallBack;
+
private final Object mSystemActionLock = new Object();
// Resource id based ActionId -> RemoteAction
@GuardedBy("mSystemActionLock")
@@ -94,7 +101,7 @@
public SystemActionPerformer(
Context context,
WindowManagerInternal windowManagerInternal) {
- this(context, windowManagerInternal, null, null);
+ this(context, windowManagerInternal, null, null, null);
}
// Used to mock ScreenshotHelper
@@ -103,17 +110,19 @@
Context context,
WindowManagerInternal windowManagerInternal,
Supplier<ScreenshotHelper> screenshotHelperSupplier) {
- this(context, windowManagerInternal, screenshotHelperSupplier, null);
+ this(context, windowManagerInternal, screenshotHelperSupplier, null, null);
}
public SystemActionPerformer(
Context context,
WindowManagerInternal windowManagerInternal,
Supplier<ScreenshotHelper> screenshotHelperSupplier,
- SystemActionsChangedListener listener) {
+ SystemActionsChangedListener listener,
+ DisplayUpdateCallBack callback) {
mContext = context;
mWindowManagerService = windowManagerInternal;
mListener = listener;
+ mDisplayUpdateCallBack = callback;
mScreenshotHelperSupplier = screenshotHelperSupplier;
mLegacyHomeAction = new AccessibilityAction(
@@ -245,6 +254,7 @@
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mSystemActionLock) {
+ mDisplayUpdateCallBack.moveNonProxyTopFocusedDisplayToTopIfNeeded();
// If a system action is registered with the given actionId, call the corresponding
// RemoteAction.
RemoteAction registeredAction = mRegisteredSystemActions.get(actionId);
@@ -341,7 +351,7 @@
int source) {
KeyEvent event = KeyEvent.obtain(downTime, time, action, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM,
- source, null);
+ source, mDisplayUpdateCallBack.getLastNonProxyTopFocusedDisplayId(), null);
mContext.getSystemService(InputManager.class)
.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
event.recycle();
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index eb71885..d9e25ef 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -199,6 +199,9 @@
case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:
mLastTouchedWindowId = event.getWindowId();
break;
+ case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
+ mAms.moveNonProxyTopFocusedDisplayToTopIfNeeded();
+ break;
}
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 969afe5..2ffbd5a 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -444,6 +444,11 @@
public abstract IBinder getFocusedWindowTokenFromWindowStates();
/**
+ * Moves the given display to the top.
+ */
+ public abstract void moveDisplayToTopIfAllowed(int displayId);
+
+ /**
* @return Whether the keyguard is engaged.
*/
public abstract boolean isKeyguardLocked();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index dde87b1..a062410 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7650,6 +7650,11 @@
}
@Override
+ public void moveDisplayToTopIfAllowed(int displayId) {
+ WindowManagerService.this.moveDisplayToTopIfAllowed(displayId);
+ }
+
+ @Override
public boolean isKeyguardLocked() {
return WindowManagerService.this.isKeyguardLocked();
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
index d9461aa..b62dbcd 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
@@ -108,6 +108,7 @@
@Mock private StatusBarManager mMockStatusBarManager;
@Mock private ScreenshotHelper mMockScreenshotHelper;
@Mock private SystemActionPerformer.SystemActionsChangedListener mMockListener;
+ @Mock private SystemActionPerformer.DisplayUpdateCallBack mMockCallback;
@Before
public void setup() {
@@ -125,7 +126,7 @@
mMockContext,
mMockWindowManagerInternal,
() -> mMockScreenshotHelper,
- mMockListener);
+ mMockListener, mMockCallback);
}
private void setupWithRealContext() {
@@ -133,7 +134,7 @@
InstrumentationRegistry.getContext(),
mMockWindowManagerInternal,
() -> mMockScreenshotHelper,
- mMockListener);
+ mMockListener, mMockCallback);
}
// We need below two help functions because AccessbilityAction.equals function only compares