Skip to content

TalkBack, 9 of 10: Improve main server list accessibility - #6167

Draft
eliotcougar wants to merge 16 commits into
2dust:masterfrom
eliotcougar:fix/talkback-main-server-rows
Draft

eliotcougar wants to merge 16 commits into
2dust:masterfrom
eliotcougar:fix/talkback-main-server-rows

Conversation

@eliotcougar

@eliotcougar eliotcougar commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Make main-screen server rows, subscription-group tabs and proxy-chain member actions easier to navigate with TalkBack, while preserving the visible controls and existing profile operations. This is part 9 of the ten focused accessibility PRs.

Server rows

  • Expose one native clickable accessibility item per server in both list and two-column layouts. Its primary action is labeled Select and invokes the existing selection/restart handler; this PR does not alter connection lifecycle behavior.
  • Announce the localized Selected prefix whenever the row matches selectedGuid, including while disconnected. This represents the selected configuration, not a separate claim about which profile the daemon is currently running.
  • Follow the prefix with the profile name and nonempty badge, description, protocol information and test result. The descendant text and action buttons do not add duplicate TalkBack swipe stops.
  • Preserve compact visual delay text. Positive results use localized singular/plural milliseconds in accessibility text, zero adds no result, and negative failure markers announce a dedicated localized Connection test failed phrase instead of a negative duration.
  • Provide custom actions in this order: Edit config + name, Delete config + name, supported share variants, then Move to top, Move up, Move down, Move to bottom. Omit moves that cannot change the item's position. As with dragging, server moves are unavailable in All or while searching.
  • Custom, policy-group and proxy-chain profiles expose only the supported full-configuration share variant. Visible menus and accessibility actions share filtering/dispatch; deletion uses the existing confirmation callback.
  • Keep visible Share/Edit/Delete/More buttons operable for ordinary touch and keyboard input. Their equivalent accessibility operations live on the row's custom-action menu.

Rows are actionable list items, not radio buttons or switches. The native activation/focus machinery remains intact; selection context is part of the localized description. The container action menu follows the documented Compose custom-action approach.

Subscription-group tabs

  • Announce the name and a localized count, for example Lab. Contains 15 servers, while retaining genuine tab role and selection semantics.
  • Keep the stable group-ID-keyed LazyRow and its native scrolling semantics so forward/backward TalkBack swipes can reach tabs outside either viewport edge.
  • Reveal a newly selected tab even when it is only partially visible. Compare its bounds with the viewport; an oversized tab is aligned to show the beginning of its name.
  • Selection-driven scrolling does not continuously follow the scroll position, so browsing unselected tabs does not snap back to the selected group.
  • Preserve the visual divider and selected indicator, including the restoration from Fix group separators without regressing TalkBack navigation #6163. Subscription-driven group-count refresh remains separate in Refresh group profile counts after subscription updates #6156.

Proxy-chain members and move feedback

  • Keep each editable member field's native text-editing behavior and attach Remove plus applicable reorder actions to that field. The visible remove icon no longer creates another accessibility stop.
  • Use stable draft keys for removal and movement, including duplicate member names. Dragging and custom actions share the same operation that moves a member together with its key. Named deletion-dialog wording remains the responsibility of part 7.
  • Successful custom moves publish a short localized Moved status through the shared window-owned polite live region; rejected/no-op moves do not. Preserve lifecycle-scoped publication and the shared helper layout used by the other list PRs.
  • No application-issued focus requests, direct speech announcements or gesture interception are added. Service/transient-notification accessibility and current/bulk connection-test announcements remain in parts 1 and 10.

Localization and review boundaries

All nine declared catalogs include the new/accessibility labels and plural resources: English, Arabic, Bengali, Bakhtiari, Persian, Russian, Vietnamese, Simplified Chinese and Traditional Chinese. The existing XML catalogs remain intact.

No native dependency, profile schema or storage-transaction-policy changes. The storage atomicity/reconciliation work in #6135 is not duplicated here. The PR retains focused JVM regression coverage; temporary Android instrumentation fixtures are kept outside the contribution rather than exposing production declarations solely for tests.

Current validation — 88e42c063

  • :app:testPlaystoreDebugUnitTest: 83 tests, no failures/errors. Includes clipped/oversized tab scroll distances, valid/invalid stable-key member moves, action ordering/filtering and existing dispatch.
  • :app:compilePlaystoreDebugKotlin and :app:assemblePlaystoreDebug -PABI_FILTERS=x86_64: passed. Existing unrelated build warnings remain.
  • External instrumentation on Pixel 6a / Android 13 / TalkBack 14.2, with TalkBack enabled and not suppressed:
    • Selecting a partially clipped tab revealed its full width.
    • Virtual-hardware touchscreen swipes traversed nine group tabs forward and backward across off-screen boundaries, without changing selection while browsing; an off-screen tab was activated successfully.
    • Negative values (-1 and Long.MIN_VALUE), zero, and positive singular/plural results exposed the expected descriptions in both list and grid layouts, with no child accessibility targets on server rows.
    • The new failure resource resolved in all nine locales.
    • A proxy-chain member move preserved its native node and accessibility focus, updated boundary actions, and saved the expected member order through production storage.
  • XML/key checks and git diff --check: passed.
  • All 20 pairwise merge simulations passed: this branch against fetched upstream and each of the other nine accessibility branches, in both orders. These are merge simulations, not a full combined APK build.

The temporary fixtures were removed, and the emulator's original APK and accessibility settings were restored. Pre-existing subscription data was retained.

Not run

  • Physical-device TalkBack gestures and spoken fluency in every locale. Resource resolution and native-node checks are not native-speaker validation.
  • The complete light/dark/dynamic-color, supported-Android-version, Switch Access and keyboard/D-pad matrix, activity-recreation/IME scenarios, or an end-to-end VPN lifecycle retest.

The gesture check used emulator-console virtual touchscreen events. Ordinary injected Android touch events bypass the accessibility input filter and were not counted as proof of TalkBack swipe traversal.

@eliotcougar eliotcougar changed the title TalkBack, 9 of N: Improve main server list accessibility TalkBack, 9 of 10: Improve main server list accessibility Aug 30, 2026
@eliotcougar
eliotcougar marked this pull request as ready for review August 30, 2026 18:07
@aliRahimi1997

Copy link
Copy Markdown
Contributor

For the main server list, I think we should preserve the behavior from PR #6108 exactly, without introducing any further changes to the server-selection semantics.

In particular:
• Do not expose each unselected server as a RadioButton.
• Do not announce "Not selected" for unselected servers.
• Keep the existing server-row interaction and accessibility behavior unchanged.
• Only the currently selected server may expose its selected/connected state where that is already supported by #6108.
• Do not add a fabricated role, stateDescription, or custom wording to the unselected rows.

Since #6108 has already been merged into master, I would prefer #6167 to leave that part of the implementation exactly as it is in the current upstream behavior rather than replacing it with selectable/RadioButton semantics.

The other accessibility improvements in #6167 can remain if they do not alter the server-selection semantics.

I tested the current combined accessibility build with TalkBack. With #6167, every unselected server is announced as "Not selected ... Radio button", which is unnecessarily verbose and makes navigating the server list much more difficult. The behavior from #6108 did not have this problem.

So for the main server list, please preserve #6108's behavior exactly rather than trying to make TalkBack's generated RadioButton announcement match the preferred wording.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

It may sound disappointing, but our previous implementation was full of unsupported hacks. That's why it was rejected.

According to Android accessibility guidelines, the app should not suppress the control role (switch, checkbox, tab, button) only to prevent it from being spoken. Even more so, the app should not take over the TalkBack job by using ClearAndSetSemantics() and constructing the spoken sentence manually (that we did a lot in many places). That function should be used sparingly, and is strongly discouraged. According to guidelines, the objective of TalkBack is not to "sound good", it is to communicate semantic state of the app UI to accessibility devices. The pattern "state - content - role" is hardcoded and the app should not mess with it.

Some of your concerns in adjacent PRs are valid and I'll try to address them.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

replacing it with selectable/RadioButton semantics.

The thing is, this time I'm not replacing it with anything. If you think about it, the server row items are, semantically, radio buttons, despite looking different: it is a list of items where only one item can be selected at a time.

@aliRahimi1997

Copy link
Copy Markdown
Contributor

I understand your point that the server rows represent a single-choice list semantically.

However, I would like to request that the accessibility behavior from #6108 be restored as-is.

In #6108, the server rows were not exposed as RadioButtons, and TalkBack did not announce "Not selected" and "Radio button" for every unselected server. This made navigating through a list of many servers significantly less verbose and much more comfortable in real-world TalkBack use.

The current implementation introduces RadioButton semantics for every server row, which causes TalkBack to announce "Not selected" and "Radio button" for every unselected server. This is unnecessarily verbose and becomes quite annoying when navigating through a large server list.

I am not asking for custom speech, ClearAndSetSemantics(), or any manual manipulation of TalkBack's output. I am specifically asking to restore the accessibility behavior that already existed in #6108, which worked well in practice.

Please restore the accessibility behavior of #6108 for the server list without introducing the RadioButton role for these rows.

@eliotcougar
eliotcougar marked this pull request as draft August 31, 2026 17:14
@eliotcougar

eliotcougar commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@aliRahimi1997 What do you think about this implementation? I cannot test it on my device at the moment...

I'm starting to think that this is the most correct approach to list items + attached buttons. Make buttons inert, and move their actions into custom actions (Swipe up/down)... According to the guidelines, this is preferred approach to navigating long lists.

aliRahimi1997 added a commit to aliRahimi1997/v2rayNG that referenced this pull request Aug 31, 2026
@aliRahimi1997

Copy link
Copy Markdown
Contributor

I think the new approach is cleaner for navigating a long server list, and I understand the rationale behind moving the actions to TalkBack's Actions menu.

My only concern is that this changes an interaction pattern that blind users have been familiar with for a long time. Some users may initially find it less convenient to access Edit/Delete/Share through the Actions menu instead of having those buttons directly available next to each server.

That said, I think the new approach is worth considering, especially if we can keep the selected-state announcement clear. The main issue I noticed in my testing is that "Selected" is currently no longer announced.

@aliRahimi1997

Copy link
Copy Markdown
Contributor

Would it be possible to keep the new Actions-menu approach while also providing a long-press alternative for users who are accustomed to interacting with the Edit/Delete/Share controls directly on each server?

I don't think this is necessarily required, but it might make the transition easier for existing blind users who are used to those actions being directly associated with each server.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

Would it be possible to keep the new Actions-menu approach while also providing a long-press alternative for users who are accustomed to interacting with the Edit/Delete/Share controls directly on each server?

I don't think this is necessarily required, but it might make the transition easier for existing blind users who are used to those actions being directly associated with each server.

In order to enable per-row navigation, the individual action buttons have to be removed from the accessibility graph. Unfortunately, they also become inaccessible directly. It's difficult to find a good compromise. Navigation through the server list is much easier, but at the cost of unfamiliar UI. Also, custom actions menu can have additional "move to top/bottom" and "move up/down" actions for reordering servers in a more accessible way.

I think it is more convenient for completely blind users, but those with some remaining vision would tap the button and it will not work as a button.

@aliRahimi1997

Copy link
Copy Markdown
Contributor

Thanks for the explanation. I understand the trade-off much better now.

I tested the current implementation with TalkBack on my device.

Overall, I think the new approach works well for navigating a long server list. The individual Edit/Delete/Share buttons are no longer part of the TalkBack swipe navigation, so swiping moves directly from one server to the next. The actions are still available through TalkBack's Actions menu, which works correctly.

I also think the additional custom actions you mentioned, such as Move up, Move down, Move to top, and Move to bottom, could be especially useful for completely blind users.

I agree that this changes the interaction model for users with some remaining vision. They may still see the Edit/Delete/Share buttons next to each server and naturally expect them to behave as buttons. I think this is worth considering because existing users may need some time to get used to the new interaction.

One possible compromise came to mind. Would it be possible to keep the Edit/Delete/Share buttons visually present for sighted and low-vision users, while keeping those individual buttons out of the TalkBack accessibility graph? TalkBack users could continue to access the same actions through the Actions menu.

Additionally, the server row could support a long-press action that opens a menu with the same actions. This could provide a familiar direct interaction for users who prefer gestures, without adding the individual action buttons back into TalkBack's swipe navigation.

One clarification about this idea: if we implement a long-press menu, would it make sense for it to expose the same full set of actions as the TalkBack Actions menu, including Move up/down and Move to top/bottom, or would those additional reordering actions be intended to remain TalkBack-only?

That would give us something like:

  • Sighted/low-vision users: visible Edit/Delete/Share buttons
  • TalkBack users: clean swipe navigation directly between server rows
  • TalkBack Actions menu: Edit/Delete/Share plus additional actions such as reordering
  • Long press on a server row: an alternative way to access the same actions
  • No additional action-button nodes in the TalkBack swipe navigation

I realize this may add some complexity, so I don't know whether it would be the best implementation. I'm mainly suggesting it as a possible compromise between the two interaction models.

There is also one issue I noticed during my testing that I think is important to preserve: the selected-state announcement.

The RadioButton announcement is now gone, which is good. However, the "Selected" announcement has disappeared as well. Previously, TalkBack announced the currently selected server as "Selected", but the current implementation no longer announces the selected state.

Ideally, I think the behavior should be:

  • Selected server → "Selected" + server information
  • Other servers → server information only
  • No "Not selected" announcement for the other servers
  • No "Radio button" announcement
  • Keep the current Actions-menu approach

The "Not selected" announcement was particularly verbose and annoying when navigating through a long list, so I would strongly prefer not to reintroduce it.

If we can preserve the selected-state indication while keeping the new per-row navigation and Actions-menu approach, I think this would provide a very good experience for completely blind users while also minimizing the impact on sighted and low-vision users.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

One clarification about this idea: if we implement a long-press menu, would it make sense for it to expose the same full set of actions as the TalkBack Actions menu, including Move up/down and Move to top/bottom, or would those additional reordering actions be intended to remain TalkBack-only?

I don't know what's the "long-press menu"... The Actions menu that you can access with three-finger tap always contains the same items as the custom actions accessible with vertical swipe.
My main hesitancy is from the way the on-screen buttons become inert. I don't know whether I like it. The quick and intuitive navigation through lists is great, though... If we decide to use it, it has to be implemented in other long lists that have action buttons (subscriptions, routing, ...)

However, the "Selected" announcement has disappeared as well.

It was there, but only when VPN is connected. Now it's fixed.

@aliRahimi1997

Copy link
Copy Markdown
Contributor

I tested the latest implementation extensively with TalkBack.

Everything works correctly from an accessibility perspective:

  • The selected server is announced as “Selected”, both when VPN is connected and disconnected.
  • Unselected servers are announced normally, without “Not selected” or “Radio button”.
  • Swipe navigation moves directly from one server to the next.
  • The TalkBack Actions menu correctly provides Edit, Delete, QR Code, Copy Link, Copy Profile Link, and Copy Full Configuration, and all of them work as expected.
  • Selecting another server correctly updates the selected announcement.
  • The selected state is still correct after completely closing the app and reopening it.

One additional thought: many blind TalkBack users are accustomed to long-pressing an item to access its available actions. I understand that the current implementation intentionally uses TalkBack Custom Actions, but would it be possible to also support a long-press action/menu on the server row, without interfering with the current Actions menu and swipe navigation?

In any case, based on my testing, the current Custom Actions implementation itself works very well for blind users.

@eliotcougar

eliotcougar commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

long-pressing an item to access its available actions.

I don't see it anywhere in the TalkBack settings. Holding is announcing the item that is currently under the finger. It does not open any menu if you hold it... The Action menu is accessed by tapping with three fingers and selecting the first menu item - Actions. Maybe we're using different TalkBack versions?

If you're suggesting to implement a separate accessibility menu for long-press, then it's not feasible as the long-press is already used to activate "drag reorder"…

@aliRahimi1997

Copy link
Copy Markdown
Contributor

Thanks for checking this and for clarifying.

What I meant by long-press was not to replace TalkBack's Actions menu, and I wasn't suggesting that the existing action buttons next to each server row should be removed either.

I was wondering if it would be possible to support all of these interaction methods together:

  • Keep the existing visible action buttons next to each server row for sighted users.
  • Keep the TalkBack Actions menu for accessibility.
  • Also make long-pressing a server row open the same set of available actions, if technically possible.

For example, a long-press could open the actions for that server, such as Edit, Delete, QR Code, Copy Link, Copy Profile Link, and Copy Full Configuration.

I think this could be useful for both sighted and TalkBack users. The visible buttons would remain available as they are now, while long-press could provide a convenient shortcut to the same actions. TalkBack's Actions menu would also remain available as the accessibility-specific way to access them.

I also think this could be particularly convenient for blind users, because long-pressing directly on the server row can be easier and more straightforward than navigating through TalkBack's Actions menu to find the available actions.

So I'm not suggesting replacing one interaction with another. The idea is simply to provide multiple ways to access the same server actions, if the existing UI architecture allows it.

eliotcougar added a commit to eliotcougar/v2rayNG that referenced this pull request Sep 2, 2026
Reuse 2dust#6167's reviewed Selected translations in the shared locale catalogs. This removes the remaining English fallback and keeps focused accessibility branches conflict-free when merged in either order.
eliotcougar added a commit to eliotcougar/v2rayNG that referenced this pull request Sep 2, 2026
Reuse 2dust#6167's reviewed Selected translations in the shared locale catalogs. This removes the remaining English fallback and keeps focused accessibility branches conflict-free when merged in either order.
@eliotcougar
eliotcougar force-pushed the fix/talkback-main-server-rows branch from 1951a54 to 56c9352 Compare September 2, 2026 00:18
@eliotcougar
eliotcougar marked this pull request as ready for review September 2, 2026 00:42
@eliotcougar
eliotcougar marked this pull request as draft September 5, 2026 11:13
@eliotcougar eliotcougar closed this Sep 5, 2026
@eliotcougar
eliotcougar deleted the fix/talkback-main-server-rows branch September 5, 2026 23:32
@eliotcougar
eliotcougar restored the fix/talkback-main-server-rows branch September 5, 2026 23:33
@eliotcougar eliotcougar reopened this Sep 5, 2026
Use one dispatcher for the existing server menu actions and carry the display name alongside the stable GUID when requesting removal. The GUID remains the deletion identity; confirmation policy stays with MainScreen.

This common prerequisite lets named confirmations and custom accessibility actions extend the same contract without conflicting or bypassing confirmation. Cover action routing, duplicate names and blank names with JVM regression tests.
Expose each server as one native selectable radio row while retaining the independent row actions. Name Share, Edit, and Delete with the target profile, use localized full delay units for speech, and replace the custom selected-state string with platform selection semantics.
Replace radio/selectable semantics with a native clickable row and one complete description. Move child-button accessibility entry points into ordered Edit, Delete and share actions, while retaining the visible controls for ordinary touch and keyboard input. Reuse existing menu filtering and dispatch so complex profiles and deletion confirmation keep their original behavior.

Prefix the selected row with localized Active server only while the existing UI reports a running service. This intentionally follows UI selection and introduces no daemon identity tracking, IPC, custom announcements or focus manipulation. Add the prefix in all nine locale catalogs.

Validation: 63 JVM tests and the Play Store debug app/instrumentation builds pass. Six native accessibility tests pass with TalkBack 17 on the Android 37.1 emulator, covering action ordering, complex types, stable focus/node identity, native activation and visible-button keyboard input. TalkBack speech logs confirm the active prefix without a selected-state phrase or radio role. Physical TalkBack gestures, all-locale audio, full rendering and VPN lifecycle matrices were not run.
Base the localized Selected prefix solely on the row GUID matching selectedGuid, rather than also requiring a running service. Remove the now-unused running-state parameter through the pager and row composables.

Replace Active server with concise localized Selected wording in all nine catalogs. Update the JVM and native accessibility tests to require the prefix before connection while preserving row identity and accessibility focus.

Validation: 63 JVM tests, Kotlin compilation, debug app assembly, and Android-test assembly passed. Six native tests passed with TalkBack enabled on the Android 37.1 emulator. The real main-screen tree showed “Selected. Flaunt g1...” next to a “Not connected” status, with other rows unprefixed.
Expose Move to top, Move up, Move down, and Move to bottom after the existing server row actions. Resolve moves by GUID at invocation time so a stale accessibility node cannot reorder the wrong server.
Move the server-delay plural and named server actions beside the existing server delay string. This leaves the shared accessibility block identical across focused PRs and avoids XML conflicts without changing any resource value.
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.
Fold proxy-chain list accessibility into the existing server-row accessibility branch. Each member field now owns Remove and applicable reorder actions, while the visual delete button and ordinal stay out of TalkBack swipe navigation. Stable member keys also keep removal and reordering attached to the intended item.
Reposition only the feature-specific resource entries within the existing strings.xml catalog so adjacent translator corrections merge independently across the focused accessibility PRs. Keep every resource name, value, placeholder and plural unchanged; do not split the catalog.
Label the existing native server-row click action Select so TalkBack can describe double-tap selection instead of generic activation. Keep selection dispatch by GUID, the selected-server prefix, ordered secondary actions and direct keyboard operation unchanged.

Acknowledge successful server-list and proxy-chain reorder actions through the shared polite live-region host with localized Moved feedback. Retain stable keys and do not request focus or announce rejected moves. Add the strings to all nine existing locale catalogs.

Validation: 76 Play Store debug JVM tests, Kotlin compilation and app/test assembly. Focused emulator checks covered repeatable move feedback and proxy-chain actions; six server-row tests additionally verified click labels in both layouts, native/custom action dispatch, selected identity and touch/Enter/Space/D-pad activation. Combined regressions passed. Not run: listening verification in every locale.
Give the host a single explicit owner instead of combining a supplied SnackbarHostState with a composition-local controller. Remove the unused LocalAppSnackbar provider. Keep this behavior-preserving wiring change shared by the notification, subscription, routing and server-row PRs so they remain independently mergeable.
Keep the shared live-region renderer, action-feedback host, and JVM regression tests identical to Parts 2 and 4. Start message expiry after publication and clear pending feedback on pause without changing server-row actions or selection semantics.
Reveal partially clipped selected group tabs using viewport bounds while retaining keyed LazyRow semantics and independent TalkBack swipe traversal. Announce negative server-probe values as a localized connection-test failure in all nine catalogs, preserving compact visual delay text.

Share stable-key proxy-chain movement between drag and custom actions. Remove redundant row forwarding and dispatcher coverage, restore private production declarations, and keep instrumentation fixtures as local QA material instead of PR scaffolding.

Validation: 83 JVM tests, Play Store debug compilation and assembly, all-locale resource checks, and an external Android 13 / TalkBack 14.2 probe. Virtual-hardware swipes traversed nine tabs in both directions; clipped-tab reveal, list/grid result wording, stable member focus and saved chain order passed. Physical-device speech fluency and the complete theme/input matrix were not run.
Keep AppSnackbarBridge beside the explicit snackbar host and the accessibility action-feedback host after rebasing the shared theme cleanup. This preserves ordinary foreground messages without changing the row custom actions or upstream stable-key proxy-chain deletion.

Validated Play Store debug compilation, manifest processing and JVM tests.
eliotcougar added a commit to eliotcougar/v2rayNG that referenced this pull request Sep 10, 2026
Reuse 2dust#6167's reviewed Selected translations in the shared locale catalogs. This removes the remaining English fallback and keeps focused accessibility branches conflict-free when merged in either order.
@eliotcougar
eliotcougar force-pushed the fix/talkback-main-server-rows branch from 88e42c0 to 15283df Compare September 10, 2026 09:37
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