Skip to content

TalkBack, 2 of 10: Improve subscription list accessibility - #6158

Open
eliotcougar wants to merge 13 commits into
2dust:masterfrom
eliotcougar:fix/accessibility-subscription-rows
Open

eliotcougar wants to merge 13 commits into
2dust:masterfrom
eliotcougar:fix/accessibility-subscription-rows

Conversation

@eliotcougar

@eliotcougar eliotcougar commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part 2 of the focused TalkBack series improves the subscription list and the subscription editor's delete action. Each subscription is one native toggle target with contextual information and ordered custom actions.

  • Announce the subscription name and locale-aware “Last updated on …” date/time, omitting the raw URL. Keep the compact visible timestamp unchanged.
  • Retain the native checked state and switch role on the whole row. The displayed switch has no separate click handler or accessibility target. The activation label describes the next operation: Enable update or Disable update.
  • Offer custom actions in this order: Edit, Delete, periodic auto-update, QR sharing, clipboard sharing, then applicable Move to top / Move up / Move down / Move to bottom commands. The periodic and share actions are omitted for local groups without a URL.
  • Keep the visible action buttons usable by touch and keyboard while removing their duplicate accessibility semantics.
  • Use stable subscription GUIDs for custom reorder commands and resolve the current position when invoked. Do not expose commands that cannot change the position.
  • Include the subscription name in delete confirmations from both the list and editor. Unnamed subscriptions use the URL host, without credentials/path/query, or a localized fallback.
  • Cover all nine maintained locales without splitting the XML catalogs.

Feedback and lifecycle

Successful custom moves and periodic-update changes receive short, polite live-region acknowledgements. Ordinary row toggles retain native state-change feedback, without an additional custom announcement.

The window-owned feedback host holds one message until its text has been published and its short presentation interval has elapsed. Subsequent actions wait in order; repeated wording receives distinct event IDs. Pausing the window clears current and pending feedback. This is a publication queue, not an attempt to control TalkBack's speech queue or duration.

Periodic-update acknowledgements use a non-replaying flow collected only while the screen is resumed. The mutation guard is released before delivery, so finishing an operation does not depend on returning to the screen. Errors retain the existing localized message path.

QR sharing from the ordinary share menu and the custom action uses the same ViewModel-owned background operation. Superseding or dismissing it cancels the prior request; the resulting dialog survives activity recreation.

Structure and integration

SubscriptionRow owns presentation and semantics; the screen owns transient dialogs and action wiring; the ViewModel owns periodic-update work and QR generation.

The shared live-region renderer, action-feedback host, and feedback-state JVM tests are kept identical where they overlap with Parts 1, 4, and 9. The renderer's publication callback is optional, leaving Part 1's existing notification behavior unchanged. Shared localized action entries are aligned to avoid catalog conflicts, including the neighboring Select entry used by Part 9.

No service lifecycle changes, main-server-list implementation changes, focus manipulation, role suppression, or direct accessibility announcement events are introduced here. Toast/snackbar fallback policy remains owned by Part 1.

Storage boundary: #6135

Storage locking, stale-write rejection, and rollback belong to #6135 and are deliberately not duplicated here. Once that API is available, this PR's periodic-update path must use its guarded updateSubscription operation, reschedule only on success, and reload persisted state. The existing storage race is not claimed fixed by this accessibility refinement.

Validation — c4b063291

  • :app:testPlaystoreDebugUnitTest: 75 tests, zero failures/errors.
  • :app:compilePlaystoreDebugKotlin and :app:assemblePlaystoreDebug: passed.
  • Matching helper revisions compiled and passed JVM tests in Parts 1, 4, and 9 as well.
  • All 20 introduced default string keys are present once, with matching placeholders, in all nine maintained catalogs.
  • Pairwise merge simulations with Part 2 and every companion branch, in both orders, were conflict-free and produced identical trees. Current upstream also merges cleanly with Part 2.
  • A subsequent compatibility fix in TalkBack, 8 of 10: Improve shared navigation and action labels #6166 resolves its adjacent MainBottomBar.kt additions with TalkBack, 10 of 10: Announce connection test feedback #6168. All 45 pairs of the ten focused branches merge in both orders with identical trees; merging all ten onto current upstream in forward and reverse order also produces identical trees.
  • The complete current-upstream integration does not yet compile: Part 4 calls SettingsManager.removeRoutingRuleset with a rule ID, while upstream's routing-ID revert restored the index-based API. This separate routing integration issue is not changed by Part 2 or the bottom-bar conflict fix.
  • Pixel 9 Pro / Android 37.1 emulator: signature-compatible install, one checkable subscription-row target, URL omission and localized date semantics, persisted touch toggle, Space/Enter/D-pad-center activation, QR sharing, and QR dialog retention through rotation. Original lab toggle and device accessibility/orientation settings restored.
  • No new or retained src/androidTest files in this PR. Deterministic feedback tests remain in the existing JVM test directory.

Not run: fresh spoken-output/live-region timing verification with TalkBack, direct custom-action invocation through TalkBack, a full theme/inset rendering matrix, and native-speaker verification of translations. TalkBack was bound on the emulator, but that alone is not reported as spoken-output verification.

@aliRahimi1997

Copy link
Copy Markdown
Contributor

I tested PR #6158 with TalkBack on a real device.

I found an accessibility issue with the decision to hide the raw subscription URL from accessibility services.

A subscription does not necessarily have a user-defined name. For example, a user may import a subscription directly from a URL without assigning a meaningful name to it. If the URL is then completely omitted from the accessibility node, multiple subscriptions can become indistinguishable.

In my testing, TalkBack announced something like:

"Import sub. Last updated on 31 August 2026, 03:18"

If the user has several subscriptions without distinct names, the date/time alone is not sufficient to identify which subscription this is.

I agree that unnecessarily exposing a long raw URL can make navigation noisy. However, when there is no meaningful subscription name, accessibility should retain some identifying information derived from the URL, for example the URL itself or an appropriate shortened/domain-based representation.

Could you please consider a fallback such as:

• Named subscription → announce the subscription name and last-updated date/time.
• Unnamed subscription → announce an identifying representation of its URL (for example its domain/host, or another sufficiently unique shortened form) together with the last-updated date/time.

The important point is that an unnamed subscription should remain distinguishable from other subscriptions through accessibility services.

I would prefer this to be handled through the semantic content exposed by the row, without changing the visible UI or introducing custom TalkBack announcements.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

A subscription does not necessarily have a user-defined name.

Wait whaaat? That doesn't sound good... I need to look at it asap... With Compose stable identifiers it can cause app to crash if the name is used as that.

@eliotcougar
eliotcougar marked this pull request as draft August 31, 2026 09:23
@eliotcougar

Copy link
Copy Markdown
Contributor Author

Unnamed subscriptions are now explicitly announced with their source domain name.
I really don't like that subscription name is optional and non-unique.

@eliotcougar
eliotcougar marked this pull request as ready for review August 31, 2026 11:30
aliRahimi1997 added a commit to aliRahimi1997/v2rayNG that referenced this pull request Aug 31, 2026
@aliRahimi1997

Copy link
Copy Markdown
Contributor

I tested the updated implementation on a real device with TalkBack.

The new accessibility feedback works correctly for a single subscription. For example:

"Update is on. import sub. Last updated on 31 August 2026, 21:42. Switch."

However, there is still an important identification problem when multiple subscriptions are imported from the clipboard.

When importing multiple subscriptions from the clipboard, they are all given the same default name: "import sub". As a result, TalkBack announces essentially the same name for every subscription, so a blind user cannot distinguish one subscription from another.

For example, multiple rows can all be announced as:

"Update is on. import sub. Last updated on ... Switch."

Could we provide a stable distinguishing identifier for subscriptions that have the same user-visible name, including the default "import sub" name?

The identifier does not necessarily need to be the full subscription URL or contain sensitive information. It just needs to make otherwise identical subscription rows distinguishable to accessibility services and TalkBack.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

I tested the updated implementation on a real device with TalkBack.

The new accessibility feedback works correctly for a single subscription. For example:

"Update is on. import sub. Last updated on 31 August 2026, 21:42. Switch."

However, there is still an important identification problem when multiple subscriptions are imported from the clipboard.

When importing multiple subscriptions from the clipboard, they are all given the same default name: "import sub". As a result, TalkBack announces essentially the same name for every subscription, so a blind user cannot distinguish one subscription from another.

For example, multiple rows can all be announced as:

"Update is on. import sub. Last updated on ... Switch."

Could we provide a stable distinguishing identifier for subscriptions that have the same user-visible name, including the default "import sub" name?

The identifier does not necessarily need to be the full subscription URL or contain sensitive information. It just needs to make otherwise identical subscription rows distinguishable to accessibility services and TalkBack.

How do you even create a subscription with no name? There is no option for importing it from clipboard, and you cannot save the subscription if the "remarks" field is empty.

I just tested the version I built yesterday, and it and it says "[sub name] [last updated on ...]" and that's it...

@aliRahimi1997

Copy link
Copy Markdown
Contributor

You're right about subscriptions with a completely empty name — I was referring to the accessibility identification problem more generally.

I tested clipboard import specifically. When multiple subscriptions are imported from the clipboard, they are all assigned the same default remarks/name: "import sub".

For example, with multiple imported subscriptions, TalkBack can announce:

"Update is on. import sub. Last updated on 31 August 2026, 21:42. Switch."

and another subscription can be announced with the same "import sub" name.

So the remaining issue is not an empty remarks field, but multiple subscription rows having the same default name and therefore being indistinguishable to a TalkBack user.

Could we consider adding a stable distinguishing identifier for duplicate subscription names, at least when the same name is used by multiple subscriptions?

@eliotcougar

Copy link
Copy Markdown
Contributor Author

I tested clipboard import specifically. When multiple subscriptions are imported from the clipboard, they are all assigned the same default remarks/name: "import sub".

I'm still not sure what you mean by clipboard import... I know you can import individual servers from clipboard on the main screen. Subscriptions, though... Does it also work? I never tried that.

Could we consider adding a stable distinguishing identifier for duplicate subscription names, at least when the same name is used by multiple subscriptions?

That will be outside the scope of accessibility work. Maybe a separate small PR that introduces smarter name assignment, or explicitly asks the user to enter the name for the imported subscription... In my opinion, non-unique names shouldn't be allowed at all, as they are sometimes used as list keys.

@eliotcougar
eliotcougar marked this pull request as draft August 31, 2026 19:07
@aliRahimi1997

Copy link
Copy Markdown
Contributor

Yes, subscription import from the clipboard does work.

On the main screen, when I select "Import from clipboard", v2rayNG can recognize a subscription URL as well as individual server configurations. When the clipboard contains a subscription URL, the app creates a subscription entry automatically and assigns it the default remarks/name "import sub".

If I import multiple subscription URLs this way, each subscription is created with the same name "import sub", so TalkBack cannot distinguish the resulting subscription rows by their names.

For example, two different subscriptions can both be announced as:
"Update is on. import sub. Last updated on ... Switch."

So the issue I reported is specifically about the subscription-import-from-clipboard flow, not individual server import.

I agree that smarter name assignment or preventing duplicate subscription names would probably be better handled in a separate PR. I mainly wanted to point out the accessibility impact of the current behavior.

@eliotcougar
eliotcougar marked this pull request as ready for review September 2, 2026 00:37
@aliRahimi1997

aliRahimi1997 commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

The current TalkBack Actions implementation works well, and I can access all the available subscription actions without any issue.

I would still like to reiterate the accessibility preference I mentioned for PR
#6167
if technically possible, it would be better to make these actions available through a long press on the corresponding subscription row as well.

For TalkBack users, long press is a more familiar, frequently used, and convenient way to access contextual actions than opening the Actions menu. Having both options available would therefore provide a faster and more natural interaction without removing the existing Actions menu.

@eliotcougar

eliotcougar commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor Author

I would still like to reiterate the accessibility preference I mentioned for PR #6167 if technically possible, it would be better to make these actions available through a long press on the corresponding subscription row as well.

There is no long press gesture in TalkBack. Holding a finger continuously announces whatever element is under the finger.
Maybe there is a difference across Android versions? On my Android 17 the Actions menu is accessed with three-finger tap, not with long press. There is no option to reconfigure the long press behavior in TalkBack settings.

@aliRahimi1997

Copy link
Copy Markdown
Contributor

I think I may not have explained what I mean by “long press” clearly.

I am not referring to a TalkBack-specific gesture where the user holds a finger on the screen. I mean the standard Android long-click behavior of the UI element itself.

For example, on the Android home screen, long-pressing an app icon opens contextual actions such as App info or Uninstall. Similarly, in WhatsApp, long-pressing a message opens contextual actions such as Reply, Forward, Copy, Share, etc.

What I mean is that, if technically possible, the subscription row could expose its existing contextual actions through the standard long-click interaction as well. The current TalkBack Actions menu can remain available; long-click would simply provide another, more direct way to access the same actions.

As a TalkBack user, I find this interaction more convenient for frequently used contextual actions.

@eliotcougar

eliotcougar commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor Author

I think I may not have explained what I mean by “long press” clearly.

I am not referring to a TalkBack-specific gesture where the user holds a finger on the screen. I mean the standard Android long-click behavior of the UI element itself.

For example, on the Android home screen, long-pressing an app icon opens contextual actions such as App info or Uninstall. Similarly, in WhatsApp, long-pressing a message opens contextual actions such as Reply, Forward, Copy, Share, etc.

What I mean is that, if technically possible, the subscription row could expose its existing contextual actions through the standard long-click interaction as well. The current TalkBack Actions menu can remain available; long-click would simply provide another, more direct way to access the same actions.

As a TalkBack user, I find this interaction more convenient for frequently used contextual actions.

I don't think that's possible as TalkBack intercepts the long press when it's enabled.
Whereas "Double-tap and hold" (the gesture you are talking about) currently leads to the existing "long press" action that is used to drag the list items. I'll see if it's possible to reroute the action based on whether TalkBack is on or off.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

@aliRahimi1997
I just asked AI to check the Android documentation. Here's what is found:

  1. TalkBack doesn't have an API for opening the Actions menu.
  2. Users can re-assign any existing TalkBack gesture to “Show custom actions”. It will be device-wide, though.
  3. Google explicitly discourages changing app behavior based on accessibility or touch-exploration state.
  4. Adding an actions menu (like the one on app icons in the Launcher) would have to be implemented for normal users as well, replacing the drag handle. I don't think the maintainer will agree to that change.

Sorry...

@aliRahimi1997

Copy link
Copy Markdown
Contributor

Thanks for checking this and for the detailed explanation. That makes sense.

I understand that changing the app's behavior based on whether TalkBack is enabled is not recommended. I also understand that the TalkBack Actions menu itself cannot be opened directly by the app.

I also tested the behavior on my Samsung phone. On the Samsung launcher, I noticed that the same item can support both interactions: a long press followed by movement starts drag/reorder, while a long press without moving shows contextual actions. I realize this may be a launcher-specific implementation and may not be directly applicable to v2rayNG, but I thought the observation might be useful.

My main question is therefore whether the existing long-press/drag implementation could expose the subscription actions through the standard accessibility long-click action, without depending on TalkBack state and without changing the normal drag behavior.

If that is not feasible with the current implementation, I completely understand.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

I also tested the behavior on my Samsung phone. On the Samsung launcher, I noticed that the same item can support both interactions: a long press followed by movement starts drag/reorder, while a long press without moving shows contextual actions. I realize this may be a launcher-specific implementation and may not be directly applicable to v2rayNG, but I thought the observation might be useful.

As I said, that behavior would have to be implemented for all users, not just in accessibility mode. It is outside the scope of this set of PRs and I don't think such change to the app UX will be accepted by the maintainer...

PS: I'm not even sure this TalkBack PR set will be merged.

@aliRahimi1997

Copy link
Copy Markdown
Contributor

I found one more accessibility issue in the "Update subscriptions" dialog.

When I open "Update subscriptions" from the subscription list, the dialog contains several Switch controls. TalkBack correctly identifies the controls and their current state, but after toggling a Switch, TalkBack does not announce the new On/Off state immediately. To hear the updated state, I have to move TalkBack focus away from the Switch and then return to it.

I noticed the same issue in the Per-app Proxy switches in #6164, which was addressed there with native switch state feedback.

Could the Switch controls in this dialog receive the same treatment, so that their updated state is exposed immediately to TalkBack while preserving the native Switch role and semantics?

This applies to the four options in this dialog, including the dependent options that can become enabled/disabled.

@eliotcougar

Copy link
Copy Markdown
Contributor Author

I found one more accessibility issue in the "Update subscriptions" dialog.

When I open "Update subscriptions" from the subscription list, the dialog contains several Switch controls. TalkBack correctly identifies the controls and their current state, but after toggling a Switch, TalkBack does not announce the new On/Off state immediately. To hear the updated state, I have to move TalkBack focus away from the Switch and then return to it.

I noticed the same issue in the Per-app Proxy switches in #6164, which was addressed there with native switch state feedback.

Could the Switch controls in this dialog receive the same treatment, so that their updated state is exposed immediately to TalkBack while preserving the native Switch role and semantics?

This applies to the four options in this dialog, including the dependent options that can become enabled/disabled.

Wow... I didn't even know about this dialog. It seems to be new.
It looks very confusing. I'll address it soon.

@eliotcougar
eliotcougar marked this pull request as draft September 5, 2026 11:11
Group each subscription's informational content into one accessibility node, omit the noisy URL, and replace the compact timestamp semantics with a localized date and time.

Keep Share, Edit, Delete, and the update switch as separate native controls with contextual labels so accessibility services retain their roles, state, and actions. Include the subscription name in deletion confirmations from both the list and edit screen.

Add the focused accessibility text to every maintained locale catalog.
Blank or whitespace-only subscription names previously left the row and
its actions without a useful identifier. Use a shared, localized
"Unnamed. <host>" fallback for the row, Share/Edit/Delete actions, update
switch, and delete confirmations in both the list and editor. Missing or
invalid URLs retain "Unnamed." alone; URL credentials, paths, and access
tokens are never included in the fallback.

Attach the accessible name to the merged row rather than its Text child:
a completely empty Text can have zero-width accessibility bounds. Clear
the visible name's descendant semantics to avoid announcing it twice.

This changes presentation only. Stored remarks, subscription GUIDs,
Compose keys, and action targets remain unchanged. Add the new label in
all nine maintained locales and seven focused regression tests.

Validation: all 64 JVM tests passed, as did Play Store debug compilation
and assembly. On the API 37.1 Pixel 9 Pro emulator, checked empty,
whitespace-only, named, and no-URL rows; action labels; Share; both delete
confirmation paths; keyboard Enter and D-pad Cancel activation. TalkBack
was enabled during the UI checks, with no app crash observed.

Not run: independent listening to TalkBack audio and touch-exploration
gesture traversal; the automated checks verify semantics and activation.
Let the subscription row own the native switch action and observable
checked state, with a passive child Switch. Continue updating subscriptions
through the ViewModel by GUID and retain independent Share/Edit/Delete
actions, name and last-updated semantics, and URL hiding.

Use localized Update is on/off stateDescription values in all nine
supported locales. Compose can then emit STATE_DESCRIPTION changes on the
focused row, addressing the missing feedback reproduced with Compose UI
1.11.4 and TalkBack 17. Do not relabel the row, manually announce each tap
or manipulate accessibility focus.

Add a regression test using an isolated temporary subscription. Verify
both toggle directions, one native action target, pointer/keyboard/D-pad
activation, named actions and persisted state after activity recreation.
Cleanup removes only the test subscription.

Validation: 64 Play Store debug unit tests passed; app and androidTest APKs
assembled; SubscriptionToggleFeedbackTest passed on the Android 37.1
Pixel 9 Pro emulator with TalkBack 17 enabled. Physical-device gestures
and pronunciation in every locale were not tested.
Keep each subscription as one native switch target, move Edit, Delete, and share variants into its ordered accessibility action menu, and add position-aware reorder actions resolved by subscription ID.
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.
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.
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.
Name the native row click action Enable update or Disable update according to its next state, retaining the row-owned switch and native checked-state feedback.

Add a periodic auto-update custom action for URL-backed subscriptions. Resolve the subscription by stable ID, validate the existing interval, persist only the requested setting change, synchronize its periodic work, and publish a localized live-region acknowledgement after completion. Report successful reorder actions with Moved, including repeated moves, without announcing failed or unavailable moves.

Keep the live-region host above screen content and supply all new strings in the nine existing locale catalogs. Do not add duplicate click targets or manually request focus.

Validation: 72 Play Store debug JVM tests, compilation and app/test assembly; three focused emulator tests for native click labels, state persistence, auto-update feedback, repeated move events and retained row focus. Combined integration tests also 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.
Deliver periodic-update acknowledgements through a non-replaying resumed-screen flow after releasing the operation guard. Queue action feedback until publication and clear it on pause. Preserve native row toggle semantics while extracting presentation, removing redundant icon labels, and generating cancellable QR results off the main thread. Remove the four instrumentation-test sources; keep and expand deterministic JVM coverage. Align shared localized action entries for cross-PR merging. Storage stale-write protection remains owned by 2dust#6135 and is deliberately not duplicated here.
Keep a single ViewModel-owned QR job and state for ordinary and accessibility sharing actions. Restore the resumed snackbar event bridge beside the explicitly supplied host, retaining periodic-update feedback and stable-key reorder actions.

Play Store debug compilation, manifest processing and JVM tests pass against current upstream.
@eliotcougar
eliotcougar force-pushed the fix/accessibility-subscription-rows branch from c4b0632 to 94bab2f Compare September 10, 2026 09:37
Reuse background QR generation and the shared named confirmation dialog. Resolve editor validation changes without replacing row semantics or custom actions; identify editor deletion by the saved subscription rather than a draft name. Drop obsolete named-confirmation and unused action resources, keeping native toggle feedback and periodic-update acknowledgements intact.
@eliotcougar
eliotcougar marked this pull request as ready for review September 21, 2026 11:27
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