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
8 changes: 8 additions & 0 deletions differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,14 @@ To let Android recognize gestures again, turn off **Advanced settings** > **Deve

## Navigation

### Returning to a screen

On phones, Backtalk remembers the last item you read on up to 20 recent app screens and browser pages while the service is running. Restoration starts only when you use Back, including the Back gesture/key or a recognized app Back button. It tries to focus the same item, including when an app recreates its list or automatically focuses the toolbar. This also works when an app opens a post or link in another app: using Back from the browser or other app restores the originating item. Forward navigation, switching apps, and opening or reading the status bar do not trigger restoration. User input on the returned screen cancels pending restoration. This memory clears when Backtalk stops.

Visible items are restored without scrolling. **Advanced settings** > **Scroll to restore previous focus** is off by default. Turn it on to let Backtalk bring an offscreen item back into view. Backtalk uses the app's supported scroll actions and checks that it found the remembered item; it stops after at most ten scroll actions or three seconds. It waits for rebuilding content instead of searching by scrolling when the saved row position is already visible. Touching, navigating, typing, or scrolling cancels pending restoration. A browser restoring its own page scroll position or updating its address during Back does not cancel the attempt.

Apps and pages must expose enough accessibility information to distinguish the screen and item. In native app rows and cards, a single exposed title or heading can identify the item even when its read-state description, counts, or subtitle change. This applies across apps; uncertain or duplicate titles still need an unambiguous match. If the item was removed, labels are ambiguous, or the app does not expose the necessary information, Backtalk keeps its usual focus behavior. It remembers elements such as headings, links, and posts, rather than character positions within text. Wear OS watches and TV keep their usual focus behavior and do not run this feature.

### Reading place kept when an item disappears

When an app removed the item that had focus, such as an ad that collapses, TalkBack jumped back to the top of the page. Backtalk carries on from where you were.
Expand Down
20 changes: 20 additions & 0 deletions lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25520,4 +25520,24 @@



<issue
id="MissingTranslation"
message="&quot;title_pref_scroll_to_restore_previous_focus&quot; is not translated in &quot;de&quot; (German), &quot;hi&quot; (Hindi), &quot;lo&quot; (Lao), &quot;pt&quot; (Portuguese), &quot;fil&quot; (Filipino; Pilipino), &quot;lt&quot; (Lithuanian), &quot;hr&quot; (Croatian), &quot;lv&quot; (Latvian), &quot;hu&quot; (Hungarian), &quot;hy&quot; (Armenian), &quot;uk&quot; (Ukrainian), &quot;id&quot; (Indonesian), &quot;ur&quot; (Urdu), &quot;mk&quot; (Macedonian), &quot;ml&quot; (Malayalam), &quot;mn&quot; (Mongolian), &quot;af&quot; (Afrikaans), &quot;in&quot; (Indonesian), &quot;mr&quot; (Marathi), &quot;uz&quot; (Uzbek), &quot;ms&quot; (Malay), &quot;el&quot; (Greek), &quot;en&quot; (English), &quot;is&quot; (Icelandic), &quot;it&quot; (Italian), &quot;am&quot; (Amharic), &quot;my&quot; (Burmese), &quot;es&quot; (Spanish), &quot;iw&quot; (Hebrew), &quot;zh&quot; (Chinese), &quot;et&quot; (Estonian), &quot;eu&quot; (Basque), &quot;ar&quot; (Arabic), &quot;as&quot; (Assamese), &quot;vi&quot; (Vietnamese), &quot;nb&quot; (Norwegian Bokmål), &quot;ja&quot; (Japanese), &quot;ne&quot; (Nepali), &quot;az&quot; (Azerbaijani), &quot;fa&quot; (Persian), &quot;zu&quot; (Zulu), &quot;ro&quot; (Romanian), &quot;nl&quot; (Dutch), &quot;no&quot; (Norwegian), &quot;be&quot; (Belarusian), &quot;fi&quot; (Finnish), &quot;ru&quot; (Russian), &quot;bg&quot; (Bulgarian), &quot;bn&quot; (Bangla), &quot;fr&quot; (French), &quot;bs&quot; (Bosnian), &quot;ka&quot; (Georgian), &quot;si&quot; (Sinhala), &quot;sk&quot; (Slovak), &quot;sl&quot; (Slovenian), &quot;ca&quot; (Catalan), &quot;sq&quot; (Albanian), &quot;sr&quot; (Serbian), &quot;kk&quot; (Kazakh), &quot;km&quot; (Khmer), &quot;kn&quot; (Kannada), &quot;or&quot; (Odia), &quot;sv&quot; (Swedish), &quot;ko&quot; (Korean), &quot;sw&quot; (Swahili), &quot;gl&quot; (Galician), &quot;ta&quot; (Tamil), &quot;gu&quot; (Gujarati), &quot;ky&quot; (Kyrgyz), &quot;pa&quot; (Punjabi), &quot;te&quot; (Telugu), &quot;th&quot; (Thai), &quot;tl&quot; (Tagalog), &quot;pl&quot; (Polish), &quot;da&quot; (Danish), &quot;tr&quot; (Turkish)"
errorLine1=" &lt;string name=&quot;title_pref_scroll_to_restore_previous_focus&quot;&gt;Scroll to restore previous focus&lt;/string&gt;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="4312"
column="11"/>
</issue>
<issue
id="MissingTranslation"
message="&quot;summary_pref_scroll_to_restore_previous_focus&quot; is not translated in &quot;de&quot; (German), &quot;hi&quot; (Hindi), &quot;lo&quot; (Lao), &quot;pt&quot; (Portuguese), &quot;fil&quot; (Filipino; Pilipino), &quot;lt&quot; (Lithuanian), &quot;hr&quot; (Croatian), &quot;lv&quot; (Latvian), &quot;hu&quot; (Hungarian), &quot;hy&quot; (Armenian), &quot;uk&quot; (Ukrainian), &quot;id&quot; (Indonesian), &quot;ur&quot; (Urdu), &quot;mk&quot; (Macedonian), &quot;ml&quot; (Malayalam), &quot;mn&quot; (Mongolian), &quot;af&quot; (Afrikaans), &quot;in&quot; (Indonesian), &quot;mr&quot; (Marathi), &quot;uz&quot; (Uzbek), &quot;ms&quot; (Malay), &quot;el&quot; (Greek), &quot;en&quot; (English), &quot;is&quot; (Icelandic), &quot;it&quot; (Italian), &quot;am&quot; (Amharic), &quot;my&quot; (Burmese), &quot;es&quot; (Spanish), &quot;iw&quot; (Hebrew), &quot;zh&quot; (Chinese), &quot;et&quot; (Estonian), &quot;eu&quot; (Basque), &quot;ar&quot; (Arabic), &quot;as&quot; (Assamese), &quot;vi&quot; (Vietnamese), &quot;nb&quot; (Norwegian Bokmål), &quot;ja&quot; (Japanese), &quot;ne&quot; (Nepali), &quot;az&quot; (Azerbaijani), &quot;fa&quot; (Persian), &quot;zu&quot; (Zulu), &quot;ro&quot; (Romanian), &quot;nl&quot; (Dutch), &quot;no&quot; (Norwegian), &quot;be&quot; (Belarusian), &quot;fi&quot; (Finnish), &quot;ru&quot; (Russian), &quot;bg&quot; (Bulgarian), &quot;bn&quot; (Bangla), &quot;fr&quot; (French), &quot;bs&quot; (Bosnian), &quot;ka&quot; (Georgian), &quot;si&quot; (Sinhala), &quot;sk&quot; (Slovak), &quot;sl&quot; (Slovenian), &quot;ca&quot; (Catalan), &quot;sq&quot; (Albanian), &quot;sr&quot; (Serbian), &quot;kk&quot; (Kazakh), &quot;km&quot; (Khmer), &quot;kn&quot; (Kannada), &quot;or&quot; (Odia), &quot;sv&quot; (Swedish), &quot;ko&quot; (Korean), &quot;sw&quot; (Swahili), &quot;gl&quot; (Galician), &quot;ta&quot; (Tamil), &quot;gu&quot; (Gujarati), &quot;ky&quot; (Kyrgyz), &quot;pa&quot; (Punjabi), &quot;te&quot; (Telugu), &quot;th&quot; (Thai), &quot;tl&quot; (Tagalog), &quot;pl&quot; (Polish), &quot;da&quot; (Danish), &quot;tr&quot; (Turkish)"
errorLine1=" &lt;string name=&quot;summary_pref_scroll_to_restore_previous_focus&quot;&gt;Scroll to the last focused item when returning to a screen.&lt;/string&gt;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="4314"
column="11"/>
</issue>
</issues>
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
import com.google.android.accessibility.talkback.flags.Flags;
import com.google.android.accessibility.talkback.focusmanagement.AccessibilityFocusMonitor;
import com.google.android.accessibility.talkback.focusmanagement.FocusProcessorForLogicalNavigation;
import com.google.android.accessibility.talkback.focusmanagement.ScreenFocusRestorer;
import com.google.android.accessibility.talkback.focusmanagement.TraversalTreeCache;
import com.google.android.accessibility.talkback.focusmanagement.interpreter.ScreenStateMonitor;
import com.google.android.accessibility.talkback.focusmanagement.interpreter.TouchExplorationInterpreter;
Expand Down Expand Up @@ -821,6 +822,7 @@ public interface GestureDetectionState {
private EventLatencyLogger eventLatencyLogger;

private UserInterface userInterface;
private @Nullable ScreenFocusRestorer screenFocusRestorer;
private DisableTalkBackDialog disableTalkbackDialog;

private Configuration lastConfiguration;
Expand Down Expand Up @@ -896,6 +898,9 @@ private float userSpeechVolume() {

@Override
public boolean onUnbind(Intent intent) {
if (screenFocusRestorer != null) {
screenFocusRestorer.shutdown();
}
LogUtils.d(TAG, "onUnbind start");
final long turningOffTime = System.currentTimeMillis();
interruptAllFeedback(/* stopTtsSpeechCompletely= */ false);
Expand Down Expand Up @@ -933,6 +938,10 @@ public boolean onUnbind(Intent intent) {

@Override
public void onDestroy() {
if (screenFocusRestorer != null) {
screenFocusRestorer.shutdown();
screenFocusRestorer = null;
}
if (userInterface != null) {
userInterface.unregisterAllListeners();
}
Expand Down Expand Up @@ -1099,6 +1108,9 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
}
Performance perf = Performance.getInstance();
EventId eventId = perf.onEventReceived(event);
if (screenFocusRestorer != null) {
screenFocusRestorer.onAccessibilityEvent(event);
}
int eventType = event.getEventType();
TraversalTreeCache.onAccessibilityEvent(event);
PreparedFocusSpeech.onAccessibilityEvent(event);
Expand Down Expand Up @@ -1314,6 +1326,9 @@ && switch (physicalKeyboardEcho) {
*/
@Override
protected final boolean onKeyEvent(KeyEvent keyEvent) {
if (screenFocusRestorer != null) {
screenFocusRestorer.onKeyEvent(keyEvent);
}
boolean result = onKeyEventInternal(keyEvent);

if (primesController != null) {
Expand Down Expand Up @@ -1498,6 +1513,9 @@ private void onGestureDebug(GestureAnalyticsEvent analyticsEvent) {

/** Called by {@link TouchInteractionMonitor} when gesture detection started. */
public void onGestureDetectionStarted() {
if (screenFocusRestorer != null) {
screenFocusRestorer.onUserInteraction();
}
if (processorPhoneticLetters != null) {
processorPhoneticLetters.cancelPhoneticLetter(EVENT_ID_UNTRACKED);
}
Expand All @@ -1507,6 +1525,9 @@ private boolean handleOnGestureById(int displayId, int gestureId) {
if (!isServiceActive() || PauseController.isPaused()) {
return false;
}
if (screenFocusRestorer != null) {
screenFocusRestorer.onUserInteraction();
}
// Return before feedback, training, menus and gesture recording. Both callback overloads
// use this entry point; ignoring an action in GestureController still reports it as handled.
if (VibrationWatchGestureSettings.shouldReserve(
Expand Down Expand Up @@ -1938,6 +1959,9 @@ private void initializeFingerprintGestureCallback() {
@Override
public void onGestureDetected(int gesture) {
if (isServiceActive() && !PauseController.isPaused() && gestureController != null) {
if (screenFocusRestorer != null) {
screenFocusRestorer.onUserInteraction();
}
Performance perf = Performance.getInstance();
EventId eventId = perf.onFingerprintGestureEventReceived(gesture);

Expand Down Expand Up @@ -2198,6 +2222,19 @@ public long getSpeechCacheWindowCounts() {
KeyboardActor keyboardActor = new KeyboardActor(this);

// Construct pipeline.
SystemActionPerformer systemActionPerformer = new SystemActionPerformer(this);
if (!FormFactorUtils.isAndroidWear() && !FormFactorUtils.isAndroidTv()) {
systemActionPerformer.setBeforeActionListener(
action -> {
if (screenFocusRestorer != null) {
if (action == GLOBAL_ACTION_BACK) {
screenFocusRestorer.requestBack();
} else {
screenFocusRestorer.onUserInteraction();
}
}
});
}
pipeline =
new Pipeline(
this,
Expand Down Expand Up @@ -2250,7 +2287,7 @@ public long getSpeechCacheWindowCounts() {
editor,
labelManager,
new NodeActionPerformer(),
new SystemActionPerformer(this),
systemActionPerformer,
languageActor,
passThroughModeActor,
new TalkBackUIActor(this),
Expand Down Expand Up @@ -2461,15 +2498,26 @@ public void onPausedChanged(boolean paused) {
// in the order they are added.
eventFilter =
new EventFilter(this, compositor, touchMonitor, globalVariables, earlyFocusSpeech);
if (!FormFactorUtils.isAndroidWear() && !FormFactorUtils.isAndroidTv()) {
screenFocusRestorer =
new ScreenFocusRestorer(
this, prefs, pipeline.getFeedbackReturner(), pipeline.getActorState().getFocusHistory());
userInterface.registerListener(screenFocusRestorer);
focuser.setBeforeActivationListener(screenFocusRestorer::rememberBeforeActivation);
}
focuser.setFocusSetListener(
(node, info, eventId, actionTime) ->
eventFilter.onAccessibilityFocusSet(
node,
info,
eventId,
actionTime,
/* continuousReading= */ fullScreenReadActor != null
&& fullScreenReadActor.isActive()));
(node, info, eventId, actionTime) -> {
eventFilter.onAccessibilityFocusSet(
node,
info,
eventId,
actionTime,
/* continuousReading= */ fullScreenReadActor != null
&& fullScreenReadActor.isActive());
if (screenFocusRestorer != null) {
screenFocusRestorer.onFocusSet(node, info, actionTime);
}
});
eventFilter.setTargetPredictor(directionNavigationActor::predictTarget);
eventFilter.setFingerDownSupplier(
() ->
Expand Down Expand Up @@ -3808,6 +3856,9 @@ private void applyTouchExplorationPreference() {

/** Turns off, or back on, what pausing Backtalk turns off besides events and feedback. */
private void onPauseChanged(boolean paused) {
if (paused && screenFocusRestorer != null) {
screenFocusRestorer.onUserInteraction();
}
if (paused) {
if (supportsTouchScreen) {
requestTouchExploration(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import com.google.android.accessibility.utils.WebInterfaceUtils;
import com.google.android.libraries.accessibility.utils.log.LogUtils;
import java.util.List;
import java.util.function.Consumer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

Expand All @@ -86,6 +87,7 @@ public class FocusActor implements UserInputEventListener {
private Pipeline.FeedbackReturner pipeline;
private ListMenuManager menuManager;
private GlobalVariables globalVariables;
private @Nullable Consumer<AccessibilityNodeInfoCompat> beforeActivationListener;

//////////////////////////////////////////////////////////////////////////////////////////////////
// Construction
Expand Down Expand Up @@ -124,6 +126,11 @@ public void setFocusSetListener(FocusManagerInternal.@Nullable FocusSetListener
focusManagerInternal.setFocusSetListener(listener);
}

public void setBeforeActivationListener(
@Nullable Consumer<AccessibilityNodeInfoCompat> listener) {
beforeActivationListener = listener;
}

public void setMenuManager(ListMenuManager menuManager) {
this.menuManager = menuManager;
}
Expand All @@ -145,6 +152,9 @@ public boolean clickNode(AccessibilityNodeInfoCompat node, EventId eventId) {
if (node == null || pipeline == null) {
return false;
}
if (beforeActivationListener != null) {
beforeActivationListener.accept(node);
}
node = Scripts.actionTarget(node, false);

if (globalVariables.supportClickableLinks() && tryClickLinks(node, eventId)) {
Expand Down Expand Up @@ -189,6 +199,9 @@ public boolean longClickNode(AccessibilityNodeInfoCompat node, EventId eventId)
if (node == null || pipeline == null) {
return false;
}
if (beforeActivationListener != null) {
beforeActivationListener.accept(node);
}
return pipeline.returnFeedback(
eventId,
Feedback.nodeAction(Scripts.actionTarget(node, true), ACTION_LONG_CLICK.getId()));
Expand All @@ -200,6 +213,9 @@ public boolean clickCurrentHierarchical(@Nullable EventId eventId) {
if (currentFocus == null) {
return false;
}
if (beforeActivationListener != null) {
beforeActivationListener.accept(currentFocus);
}
AccessibilityNodeInfoCompat nodeToClick =
AccessibilityNodeInfoUtils.getSelfOrMatchingAncestor(
currentFocus, AccessibilityNodeInfoUtils.FILTER_CLICKABLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import java.util.function.IntConsumer;

/** System/global action performer */
public final class SystemActionPerformer {
Expand Down Expand Up @@ -50,12 +51,16 @@ public final class SystemActionPerformer {

/** Actor-state passed in from pipeline. */
private ActorStateWritable actorState;
private IntConsumer beforeActionListener;

public SystemActionPerformer(AccessibilityService service) {
this.service = service;
}

public boolean performAction(int id) {
if (beforeActionListener != null) {
beforeActionListener.accept(id);
}
if (actorState != null) {
actorState.setLastSystemAction(id);
}
Expand Down Expand Up @@ -83,4 +88,8 @@ public boolean performAction(int id) {
public void setActorState(ActorStateWritable actorState) {
this.actorState = actorState;
}

public void setBeforeActionListener(IntConsumer listener) {
beforeActionListener = listener;
}
}
Loading
Loading