Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2549,7 +2549,8 @@ public boolean resettingNodeCursor() {
windowEventInterpreter.addListener(uiChangeEventInterpreter);
windowEventInterpreter.addListener(imageCaptioner);
directTouchController =
new DirectTouchController(this, prefs, pipeline.getFeedbackReturner());
new DirectTouchController(
this, prefs, pipeline.getFeedbackReturner(), windowEventInterpreter);
windowEventInterpreter.addListener(directTouchController);
ringerModeAndScreenMonitor.addScreenChangedListener(directTouchController);
displayMonitor.addDisplayStateChangedListener(directTouchController);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.FOCUS_INPUT;
import static androidx.core.view.accessibility.AccessibilityWindowInfoCompat.TYPE_INPUT_METHOD;
import static com.google.android.accessibility.utils.AccessibilityWindowInfoUtils.WINDOW_ID_NONE;
import static com.google.android.accessibility.utils.traversal.TraversalStrategy.SEARCH_FOCUS_FORWARD;

import android.accessibilityservice.AccessibilityService;
Expand Down Expand Up @@ -135,6 +136,17 @@ private boolean isOnTopWindow(
return (currentActive.getLayer() > lastWindow.getLayer()) && activeRect.intersect(lastRect);
}

/**
* Whether the node is still in the window, as {@link AccessibilityNodeInfoUtils#isInWindow}
* checks, with the window's root fetched already.
*/
private static boolean isInWindow(
AccessibilityNodeInfoCompat node, int windowId, AccessibilityNodeInfoCompat windowRoot) {
int nodeWindowId = node.getWindowId();
return (nodeWindowId == WINDOW_ID_NONE || nodeWindowId == windowId)
&& AccessibilityNodeInfoUtils.hasDescendant(windowRoot, node);
}

/** Restores last focus from {@link AccessibilityFocusActionHistory} to the active window. */
public boolean restoreLastFocusedNode(ScreenState screenState, EventId eventId) {

Expand Down Expand Up @@ -175,12 +187,6 @@ public boolean restoreLastFocusedNode(ScreenState screenState, EventId eventId)
return false;
}

AccessibilityNodeInfoCompat root =
AccessibilityWindowInfoUtils.getRootCompat(currentActiveWindow);
if (root == null) {
return false;
}

int windowId = currentActiveWindow.getId();
int windowType = AccessibilityWindowInfoUtils.getType(currentActiveWindow);
WindowIdentifier windowIdentifier = WindowIdentifier.create(windowId, screenState);
Expand All @@ -202,6 +208,14 @@ public boolean restoreLastFocusedNode(ScreenState screenState, EventId eventId)
return false;
}

// Fetching the root waits for the window's app, which is drawing the new screen, so it is
// fetched only when there is a focus to restore, and a single time.
AccessibilityNodeInfoCompat root =
AccessibilityWindowInfoUtils.getRootCompat(currentActiveWindow);
if (root == null) {
return false;
}

long startTime = primesController.getTime();

AccessibilityNodeInfoCompat nodeToRestoreFocus =
Expand All @@ -221,7 +235,7 @@ public boolean restoreLastFocusedNode(ScreenState screenState, EventId eventId)
// b/365017291: Only check native nodes because sometimes the WebView is gone in the node
// tree.
|| (!WebInterfaceUtils.isWebContainer(nodeToRestoreFocus)
&& !AccessibilityNodeInfoUtils.isInWindow(nodeToRestoreFocus, currentActiveWindow))) {
&& !isInWindow(nodeToRestoreFocus, windowId, root))) {
LogUtils.e(TAG, "Do not restore focus from the invalid node " + nodeToRestoreFocus);
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class DirectTouchController(
private val service: AccessibilityService,
private val prefs: SharedPreferences,
private val feedback: Pipeline.FeedbackReturner,
/** Keeps the package of each window's root, which every look at the screen needs. */
private val windowEventInterpreter: WindowEventInterpreter,
) :
WindowEventInterpreter.WindowEventHandler,
RingerModeAndScreenMonitor.ScreenChangedListener,
Expand Down Expand Up @@ -222,8 +224,7 @@ class DirectTouchController(
return
}
val windows = service.windows.orEmpty()
val mainPackage =
windows.firstOrNull { it.id == mainWindowId }?.root?.packageName?.toString()
val mainPackage = windows.firstOrNull { it.id == mainWindowId }?.let { rootPackage(it) }
val directTyping = mainPackage != null && DirectTouchSettings.isDirectTyping(prefs, mainPackage)
val shouldBeActive = mainPackage != null && shouldBeActive(windows, mainPackage)
val navBarDirect = isNavBarDirect()
Expand Down Expand Up @@ -349,9 +350,12 @@ class DirectTouchController(
}
}

/** The package of the window's root, which the window's app is asked for once per window. */
private fun rootPackage(window: AccessibilityWindowInfo): String? =
windowEventInterpreter.getRootPackageName(window)?.toString()

private fun isSystemUiWindow(window: AccessibilityWindowInfo): Boolean =
window.type == AccessibilityWindowInfo.TYPE_SYSTEM &&
window.root?.packageName?.toString() == SYSTEM_UI
window.type == AccessibilityWindowInfo.TYPE_SYSTEM && rootPackage(window) == SYSTEM_UI

private fun shouldBeActive(
windows: List<AccessibilityWindowInfo>,
Expand All @@ -360,8 +364,11 @@ class DirectTouchController(
if (mainPackage in IGNORED_PACKAGES) {
return false
}
val capable = DirectTouchCapability.declaresDirectTouch(service.packageManager, mainPackage)
DirectTouchSettings.onFirstSight(prefs, mainPackage, capable)
// Only the first sight of an app uses its manifest, which the package manager has to read.
if (!DirectTouchSettings.isSeen(prefs, mainPackage)) {
val capable = DirectTouchCapability.declaresDirectTouch(service.packageManager, mainPackage)
DirectTouchSettings.onFirstSight(prefs, mainPackage, capable)
}
// A dialog is a window of its own, so any other application window is either a dialog from the
// same app or another app.
val otherApplicationWindows =
Expand All @@ -371,11 +378,10 @@ class DirectTouchController(
masterEnabled = DirectTouchSettings.isMasterEnabled(prefs),
appEnabled = DirectTouchSettings.isAppEnabled(prefs, mainPackage),
screenInteractive = screenInteractive && displayOn,
foreignAppWindow =
otherApplicationWindows.any { it.root?.packageName?.toString() != mainPackage },
systemUiCoversHalf = systemUiCoversHalf(windows),
dialogShowing = otherApplicationWindows.isNotEmpty(),
textFieldFocused = textFieldFocused,
foreignAppWindow = { otherApplicationWindows.any { rootPackage(it) != mainPackage } },
systemUiCoversHalf = { systemUiCoversHalf(windows) },
)
return DirectTouchPolicy.shouldBeActive(input)
}
Expand All @@ -384,11 +390,10 @@ class DirectTouchController(
val displayHeight = displayBounds().height()
val bounds = Rect()
return windows.any { window ->
if (!isSystemUiWindow(window)) {
return@any false
}
window.getBoundsInScreen(bounds)
DirectTouchPolicy.coversHalfScreen(window.isActive, bounds.height(), displayHeight)
// Telling System UI apart asks the window's app for its root, so the shape comes first.
DirectTouchPolicy.coversHalfScreen(window.isActive, bounds.height(), displayHeight) &&
isSystemUiWindow(window)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@

package com.google.android.accessibility.talkback.directtouch

/** What is on screen and how the user set things up, as far as direct touch cares. */
/**
* What is on screen and how the user set things up, as far as direct touch cares. The two checks
* that ask apps for their windows' roots are functions, so that they run only when the rest leave
* the answer open.
*/
data class DirectTouchInput(
val masterEnabled: Boolean,
/** The app in front is turned on in the direct touch list. */
val appEnabled: Boolean,
val screenInteractive: Boolean,
/** Another app's window is on top of the game. */
val foreignAppWindow: Boolean,
/** The notification shade or quick settings cover at least half the screen. */
val systemUiCoversHalf: Boolean,
val dialogShowing: Boolean,
val textFieldFocused: Boolean,
/** Another app's window is on top of the game. */
val foreignAppWindow: () -> Boolean,
/** The notification shade or quick settings cover at least half the screen. */
val systemUiCoversHalf: () -> Boolean,
)

object DirectTouchPolicy {
Expand All @@ -45,8 +49,8 @@ object DirectTouchPolicy {
input.masterEnabled &&
input.appEnabled &&
input.screenInteractive &&
!input.foreignAppWindow &&
!input.systemUiCoversHalf &&
!input.dialogShowing &&
!input.textFieldFocused
!input.textFieldFocused &&
!input.foreignAppWindow() &&
!input.systemUiCoversHalf()
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ object DirectTouchSettings {
prefs.edit().putBoolean(PREF_TYPING_PREFIX + pkg, enabled).apply()
}

/** Whether [pkg] has been seen, after which [onFirstSight] changes nothing for it. */
fun isSeen(prefs: SharedPreferences, pkg: String): Boolean = pkg in stringSet(prefs, PREF_SEEN)

/**
* Records that [pkg] has been seen. The first time, an app that [declaresCapability] is turned
* on. Later calls change nothing, so an app the user turned off stays off.
Expand Down
Loading
Loading