TalkBack, 2 of 10: Improve subscription list accessibility - #6158
eliotcougar wants to merge 13 commits into
Conversation
|
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. 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. |
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. |
|
Unnamed subscriptions are now explicitly announced with their source domain name. |
|
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... |
|
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? |
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.
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. |
|
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: 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. |
|
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 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. |
There is no long press gesture in TalkBack. Holding a finger continuously announces whatever element is under the finger. |
|
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. |
|
@aliRahimi1997
Sorry... |
|
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. |
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. |
|
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. |
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.
c4b0632 to
94bab2f
Compare
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.
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.
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
SubscriptionRowowns 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
updateSubscriptionoperation, 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:compilePlaystoreDebugKotlinand:app:assemblePlaystoreDebug: passed.MainBottomBar.ktadditions 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.SettingsManager.removeRoutingRulesetwith 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.src/androidTestfiles 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.