Skip to content

TalkBack, 6 of 10: Improve per-app proxy accessibility - #6164

Draft
eliotcougar wants to merge 6 commits into
2dust:masterfrom
eliotcougar:fix/accessibility-per-app-controls
Draft

eliotcougar wants to merge 6 commits into
2dust:masterfrom
eliotcougar:fix/accessibility-per-app-controls

Conversation

@eliotcougar

@eliotcougar eliotcougar commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Make the Per-app Proxy screen and shared app picker easier to operate with accessibility services, using genuine Compose toggle semantics. Keep routing context distinct from checkbox membership and leave service routing behavior unchanged.

Accessibility approach

  • Each header option is one toggleable row with Role.Switch; its visible Material switch has onCheckedChange = null.
  • Each app row is one toggleable row with Role.Checkbox. Tapping the row or visible checkbox area invokes the same selection operation; the checkbox is not a second action target.
  • Native checked state, role, and activation remain available. An observable, app-localized stateDescription provides state-change feedback without moving focus or posting unconditional announcements.
  • Header state descriptions are localized On/Off. App state descriptions combine localized Checked/Not checked with the routing context. The app name is the label; the visually displayed package ID is excluded from accessibility output.
  • The information icon opens the existing shared ConfirmDialog: the localized explanation, no title, and one OK button. Its rememberSaveable visibility survives activity recreation.

Example semantic content is Firefox as the label and Checked. routed through the app as the state description, with the native Checkbox role. This is not a promise of a fixed spoken order: TalkBack controls role wording, ordering, and interaction hints. State text is app-localized; role text belongs to the accessibility service.

Accurate routing context

Routing context cannot be inferred from the checkbox alone: service mode, empty selections, shared UIDs, and own-package exclusions affect configured inclusion.

PerAppRouting describes configured app inclusion, not a live connection, successful forwarding, or the outcome of individual destination/DNS rules. It uses the complete installed-app snapshot, saved package selection, UID, and service mode:

Configuration Routing context
Per-app option off, or proxy-only service mode Per-app routing is not enabled
VPN allowlist with eligible installed entries Selected UIDs through the app; other UIDs directly
VPN with an empty saved selection Other UIDs through the app; the app's own UID directly
VPN nonempty allowlist containing only self or removed packages Android's no-successful-allowlist-entry behavior: all apps included
VPN bypass list Selected UIDs and the app's own UID directly; other UIDs through the app
Root allowlist Selected UIDs through the app except the app's own UID; an empty selection includes none
Root bypass list UIDs through the app except selected UIDs and the app's own UID

Checkbox membership stays package-based. Routing context is UID-based, so an unchecked package can share the routing outcome of a checked sibling. The helper preserves the different self-package handling of VPN and root mode. Missing UID metadata omits the routing sentence instead of guessing.

The existing background package enumeration now retains ApplicationInfo.uid in AppInfo; no second package query is introduced. Filtering the displayed list does not change the routing decision. The ViewModel owns the observable full snapshot and refreshes service mode on resume, using the same root-before-VPN priority as service selection.

No VPN/root policy, selection persistence, import/export behavior, or traffic handling is changed.

Row structure and layout

  • Let the app-name Text merge naturally into the toggleable row instead of hiding it and reconstructing it as a content description.
  • Remove redundant explicit descendant merging; toggleable already merges its content.
  • Preserve Material minimum layout space on the inert child controls. Headers remain 48 dp and app rows 72 dp in the tested configuration.
  • Remove the two synthetic Android instrumentation test files. Keep focused JVM routing tests; device probes remain outside the PR.

Localization and independence

The PR has eight accessibility resource keys in each of the nine maintained catalogs: English, Arabic, Bengali, Bakhtiari, Persian, Russian, Vietnamese, Simplified Chinese, and Traditional Chinese. Key uniqueness and routing-state placeholders were checked. Routing and state descriptions use the app locale. Russian retains “маршрутизируется через приложение”.

The shared app picker gains the same one-checkbox row, app-name label, and package-ID suppression, without Per-app Proxy routing text. The OK-only modal and named callback remain aligned with #6157. This branch does not include its notification/live-region layer or the other screen-specific accessibility changes.

At commit bfbac0b16, pairwise git merge-tree checks on September 5 against fetched upstream/master and all nine companion accessibility branches passed in both orders with identical result trees. This is a conflict check, not a complete combined-build test.

Validation at bfbac0b16

  • PerAppAccessibilityTest: 13 tests covering disabled/proxy-only mode, allow/bypass lists, VPN/root empty selections, self and shared UIDs, installed/removed selections, and missing UID metadata.
  • :app:testPlaystoreDebugUnitTest: 70 tests, no failures/errors/skips.
  • :app:compilePlaystoreDebugKotlin and :app:assemblePlaystoreDebug -PABI_FILTERS=x86_64: passed.
  • API 37.1 emulator with TalkBack enabled: inspected native labels, hidden package IDs, single toggle/action targets, control dimensions, and CONTENT_CHANGE_TYPE_STATE_DESCRIPTION events. Native accessibility clicks retain focus and update state. Touch on row/control, Enter, Space, and D-pad center each toggle the same controls.
  • Verified empty VPN selection and selected/unselected allowlist/bypass descriptions in the real screen; reopened the screen to check restored original selections. Verified the information modal remains open across rotation and dismisses with OK.
  • All nine locale catalogs and git diff --check: passed.
  • Not run: recorded TalkBack audio/complete swipe traversal; physical VPN/root traffic verification; older-Android checks; exhaustive generic-picker, theme/dynamic-color, column-layout, and loading/error-state matrices. The native-event checks do not establish exact spoken ordering or packet routing.

Android guidance

Expose the header options and each app row as one native control, add localized routing context without package IDs, and replace the transient information toast with a readable dialog.
@eliotcougar eliotcougar changed the title TalkBack, 6 of N: Improve per-app proxy accessibility TalkBack, 6 of 10: Improve per-app proxy accessibility Aug 30, 2026
@eliotcougar
eliotcougar marked this pull request as ready for review August 30, 2026 17:45
…essibility

Retain the original button call site and name the existing modal-opening callback locally. This avoids an adjacent-hunk conflict between the independent modal replacement in 2dust#6157 and the switch semantics in this PR, without changing behavior or importing notification code.
@aliRahimi1997

Copy link
Copy Markdown
Contributor

I tested PR #6164 on a real device with TalkBack enabled.

The two Switch controls in the per-app proxy screen expose their current state correctly when TalkBack navigates to them.

However, after toggling either Switch, TalkBack does not immediately announce the new On/Off state. To hear the updated state, I have to navigate away from the Switch and then return to it.

I observed the same behavior with the Switch controls in PR #6161, so this may be a shared issue with the toggle semantics rather than something specific to the per-app proxy screen.

Could you please check whether the native Compose/Material Switch semantics can expose the updated state after the toggle while preserving the native Switch role, state, and action?

I am not suggesting a custom announcement or manual accessibility-focus manipulation; the goal is simply for the semantic state change to be exposed correctly to the accessibility service.

Apart from this, I did not observe any other accessibility problems with the two per-app proxy Switches.

Retain one native toggleable row with a passive child switch or checkbox.
Expose the two switches' observed checked state through stateDescription
so Compose sends the STATE_DESCRIPTION event required for immediate
feedback on the tested TalkBack 17 / Compose UI 1.11.4 combination.

Keep app names stable and move checked state plus routing context into
stateDescription instead of changing the accessible label after a toggle.
Preserve native roles, package-ID hiding and every enabled/bypass routing
combination. Update all nine locale catalogs without splitting resources.
No custom announcements or focus manipulation are added.

Add native-event tests for both switch directions, app checkbox feedback
in all four enabled/bypass combinations, one action target and pointer,
Enter, Space and D-pad input.

Validation: 60 Play Store debug unit tests passed; app and androidTest APKs
assembled; both PerAppToggleFeedbackTest cases passed on the Android 37.1
Pixel 9 Pro emulator with TalkBack 17 enabled. Checked/routing feedback was
observed in TalkBack output. Physical-device gestures and pronunciation in
every locale were not tested.
@aliRahimi1997

Copy link
Copy Markdown
Contributor

One small usability observation about the Per-App Proxy checkboxes:

TalkBack currently announces an item like:

"Checked. routed through the app. WhatsApp Business. Tick box."

From a real-world TalkBack navigation perspective, I find this order less natural because the app name is announced only after the state and description.

Would it be possible for the checkbox semantics to expose the app name as the primary label, so TalkBack can naturally announce it more like:

"WhatsApp Business. Checked. routed through the app. Tick box."

@eliotcougar

Copy link
Copy Markdown
Contributor Author

Would it be possible for the checkbox semantics to expose the app name as the primary label, so TalkBack can naturally announce it more like:

I was thinking about it too. I don't really know why Android does it like that, but there is no real control over it. It is always "State - Name - Role", in this order only. Changing it requires rebuilding everything manually and removing the semantics. TalkBack will see a list of some items, but it will not be aware that they are checkboxes, or whether they are checked or unchecked. They will be just clickable items that say something when selected and do something when double-tapped.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

"Checked. routed through the app. WhatsApp Business. Tick box."

In this example it's like this.
State: "Checked. routed through the app."
Name: "WhatsApp Business."
Role: "Tick box."

The only way I see here is to attach "routed through the app" to the name. Which goes against intended meaning of State and Name.

Adopt the overlapping terminology corrections from PR 2dust#6169 and carry them into branch-only plural and accessibility resources. This keeps later merge resolution from restoring stale Bakhtiari wording.
Derive accessibility routing context from the service mode, complete installed-app snapshot, and resolved UIDs instead of checkbox state alone. Preserve the different VPN and root empty-selection rules, own-package exclusions, shared UIDs, and missing-package behavior without changing routing policy.

Keep one native toggle target per row, merge the app-name Text naturally, and retain the Material control footprint after making the child controls inert. Remove the synthetic Android test harness and keep focused JVM coverage for the routing decision table.

Validation: 70 JVM tests, including 13 per-app routing tests; Play Store debug compilation and x86_64 assembly; native accessibility-tree and state-change checks with TalkBack enabled on API 37.1, including touch, Enter, Space, and D-pad activation. Exact TalkBack audio and root traffic forwarding were not recorded.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants