From 795baf61418bd9770e870af122f3559ab21a09fc Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Sat, 22 Aug 2026 22:05:32 -0700 Subject: [PATCH 1/6] Add accessibility announcements --- .../quantumbadger/redreader/common/General.kt | 11 ++++++++++ .../fragments/CommentListingFragment.java | 9 +++++++- .../reddit/api/RedditAPICommentAction.java | 22 +++++++++++++++++++ .../redreader/reddit/api/RedditPostActions.kt | 11 ++++++++++ src/main/res/values/strings.xml | 7 ++++++ 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/quantumbadger/redreader/common/General.kt b/src/main/java/org/quantumbadger/redreader/common/General.kt index ee7448c97..9c5cf1839 100644 --- a/src/main/java/org/quantumbadger/redreader/common/General.kt +++ b/src/main/java/org/quantumbadger/redreader/common/General.kt @@ -190,6 +190,17 @@ object General { } } + // announceForAccessibility was deprecated in Baklava, but the suggested + // replacements don't cover brief action confirmations: + // https://issuetracker.google.com/issues/425271162 + @Suppress("DEPRECATION") + @JvmStatic + fun announceForAccessibility(view: View, textRes: Int) { + runOnUiThread { + view.announceForAccessibility(view.context.getString(textRes)) + } + } + @JvmStatic fun isTablet(context: Context) = when (PrefsUtility.appearance_twopane()) { AppearanceTwopane.AUTO -> context.resources.configuration.screenLayout and diff --git a/src/main/java/org/quantumbadger/redreader/fragments/CommentListingFragment.java b/src/main/java/org/quantumbadger/redreader/fragments/CommentListingFragment.java index 6bb47a615..f54b1d741 100644 --- a/src/main/java/org/quantumbadger/redreader/fragments/CommentListingFragment.java +++ b/src/main/java/org/quantumbadger/redreader/fragments/CommentListingFragment.java @@ -321,11 +321,12 @@ public void handleCommentVisibilityToggle(final RedditCommentView view) { if(item.isComment()) { final RedditRenderableComment comment = item.asComment(); + final boolean nowCollapsed = !comment.isCollapsed(changeDataManager); changeDataManager.markHidden( TimestampUTC.now(), comment.getIdAndType(), - !comment.isCollapsed(changeDataManager)); + nowCollapsed); mCommentListingManager.updateHiddenStatus(); @@ -336,6 +337,12 @@ public void handleCommentVisibilityToggle(final RedditCommentView view) { if(position == layoutManager.findFirstVisibleItemPosition()) { layoutManager.scrollToPositionWithOffset(position, 0); } + + General.announceForAccessibility( + view, + nowCollapsed + ? R.string.accessibility_announcement_comment_collapsed + : R.string.accessibility_announcement_comment_expanded); } } diff --git a/src/main/java/org/quantumbadger/redreader/reddit/api/RedditAPICommentAction.java b/src/main/java/org/quantumbadger/redreader/reddit/api/RedditAPICommentAction.java index f00696fa0..14aa5e8da 100644 --- a/src/main/java/org/quantumbadger/redreader/reddit/api/RedditAPICommentAction.java +++ b/src/main/java/org/quantumbadger/redreader/reddit/api/RedditAPICommentAction.java @@ -636,6 +636,28 @@ protected void onSuccess() { if(action == RedditAPI.ACTION_DELETE) { General.quickToast(context, R.string.delete_success); } + + if(!vote) { + return; + } + + final int announcementRes; + + switch(action) { + case RedditAPI.ACTION_UPVOTE: + announcementRes = R.string.accessibility_announcement_upvoted; + break; + case RedditAPI.ACTION_DOWNVOTE: + announcementRes = R.string.accessibility_announcement_downvoted; + break; + default: + announcementRes = R.string.accessibility_announcement_removed_vote; + break; + } + + General.announceForAccessibility( + activity.getWindow().getDecorView(), + announcementRes); } private void revertOnFailure() { diff --git a/src/main/java/org/quantumbadger/redreader/reddit/api/RedditPostActions.kt b/src/main/java/org/quantumbadger/redreader/reddit/api/RedditPostActions.kt index a3de4a6ad..c9332fc24 100644 --- a/src/main/java/org/quantumbadger/redreader/reddit/api/RedditPostActions.kt +++ b/src/main/java/org/quantumbadger/redreader/reddit/api/RedditPostActions.kt @@ -1138,6 +1138,17 @@ object RedditPostActions { else -> throw java.lang.RuntimeException("Unknown post action") } + + if (vote) { + General.announceForAccessibility( + activity.window.decorView, + when (action) { + RedditAPI.ACTION_UPVOTE -> R.string.accessibility_announcement_upvoted + RedditAPI.ACTION_DOWNVOTE -> R.string.accessibility_announcement_downvoted + else -> R.string.accessibility_announcement_removed_vote + } + ) + } } private fun revertOnFailure() { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 2ceccd41e..f438a87a0 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1953,4 +1953,11 @@ pref_about_github Source code https://github.com/QuantumBadger/RedReader + + + Upvoted + Downvoted + Removed vote + Collapsed comment + Expanded From b9e096b6bb1f483038e00940aaccc4a5f3d6de36 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Sat, 22 Aug 2026 23:08:38 -0700 Subject: [PATCH 2/6] Enforce length limit on accessibility announcements --- src/main/res/values/strings.xml | 5 + .../AccessibilityAnnouncementLengthTest.java | 126 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/test/java/org/quantumbadger/redreader/test/general/AccessibilityAnnouncementLengthTest.java diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f438a87a0..c03368a78 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1955,6 +1955,11 @@ https://github.com/QuantumBadger/RedReader + Upvoted Downvoted Removed vote diff --git a/src/test/java/org/quantumbadger/redreader/test/general/AccessibilityAnnouncementLengthTest.java b/src/test/java/org/quantumbadger/redreader/test/general/AccessibilityAnnouncementLengthTest.java new file mode 100644 index 000000000..68ca3ba83 --- /dev/null +++ b/src/test/java/org/quantumbadger/redreader/test/general/AccessibilityAnnouncementLengthTest.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * This file is part of RedReader. + * + * RedReader is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RedReader is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RedReader. If not, see . + ******************************************************************************/ + +package org.quantumbadger.redreader.test.general; + +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.io.File; +import java.util.ArrayList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class AccessibilityAnnouncementLengthTest { + + // TalkBack only reports an announcement uninterruptibly when it is at most + // VERBOSE_UTTERANCE_THRESHOLD_CHARACTERS (30) characters long. Longer + // announcements are merely queued, and the queue is flushed when + // accessibility focus moves, so an over-length announcement made just + // before a focus change (such as an action confirmation) is discarded + // silently. This test keeps every locale's announcement strings under + // the limit. + private static final int TALKBACK_UNINTERRUPTIBLE_MAX_LENGTH = 30; + + private static final String ANNOUNCEMENT_PREFIX = "accessibility_announcement_"; + + @Test + public void announcementStringsFitTalkBackThreshold() throws Exception { + + final File resDir = new File("src/main/res"); + + assertTrue( + "Resource directory not found: " + resDir.getAbsolutePath(), + resDir.isDirectory()); + + final File[] valueDirs = resDir.listFiles( + (dir, name) -> name.startsWith("values")); + + assertNotNull(valueDirs); + + final DocumentBuilder builder + = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + + final ArrayList failures = new ArrayList<>(); + int checked = 0; + + for(final File valueDir : valueDirs) { + + final File stringsFile = new File(valueDir, "strings.xml"); + + if(!stringsFile.isFile()) { + continue; + } + + final Document document = builder.parse(stringsFile); + final NodeList strings = document.getElementsByTagName("string"); + + for(int i = 0; i < strings.getLength(); i++) { + + final Element element = (Element)strings.item(i); + final String name = element.getAttribute("name"); + + if(!name.startsWith(ANNOUNCEMENT_PREFIX)) { + continue; + } + + final String text = unescapeAndroidString(element.getTextContent()); + checked++; + + if(text.length() > TALKBACK_UNINTERRUPTIBLE_MAX_LENGTH) { + failures.add(valueDir.getName() + "/" + name + " is " + + text.length() + " characters: \"" + text + "\""); + } + } + } + + assertTrue( + "No announcement strings found; has the naming prefix changed?", + checked > 0); + + assertTrue( + "Announcement strings longer than TalkBack's uninterruptible " + + "limit of " + TALKBACK_UNINTERRUPTIBLE_MAX_LENGTH + + " characters (these would be silently discarded when " + + "accessibility focus moves):\n" + + String.join("\n", failures), + failures.isEmpty()); + } + + private static String unescapeAndroidString(final String s) { + + String res = s.trim(); + + if(res.length() >= 2 + && res.startsWith("\"") + && res.endsWith("\"")) { + res = res.substring(1, res.length() - 1); + } + + return res + .replace("\\'", "'") + .replace("\\\"", "\"") + .replace("\\n", "\n") + .replace("\\t", "\t"); + } +} From ff357b52b87b4c9d9840b68698743711c019dac9 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Sun, 23 Aug 2026 02:43:04 -0700 Subject: [PATCH 3/6] Suppress text changes on post and comment views with a custom content description --- .../quantumbadger/redreader/common/General.kt | 34 +++++++++++++++++++ .../redreader/views/RedditCommentView.java | 1 + .../redreader/views/RedditPostHeaderView.java | 1 + .../redreader/views/RedditPostView.java | 2 ++ 4 files changed, 38 insertions(+) diff --git a/src/main/java/org/quantumbadger/redreader/common/General.kt b/src/main/java/org/quantumbadger/redreader/common/General.kt index 9c5cf1839..98ce739d1 100644 --- a/src/main/java/org/quantumbadger/redreader/common/General.kt +++ b/src/main/java/org/quantumbadger/redreader/common/General.kt @@ -36,9 +36,12 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.view.WindowManager.BadTokenException +import android.view.accessibility.AccessibilityEvent import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.ViewCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.quantumbadger.redreader.BuildConfig import org.quantumbadger.redreader.R @@ -201,6 +204,37 @@ object General { } } + // TalkBack reads a view's raw text aloud when it changes under accessibility + // focus, ignoring any content description. Install this on views whose content + // description already covers their text; description changes still go through. + @JvmStatic + fun suppressAccessibilityTextChangeEvents(view: View) { + ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() { + override fun sendAccessibilityEventUnchecked( + host: View, + event: AccessibilityEvent + ) { + if (event.eventType == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) { + return + } + + if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + val changeTypes = event.contentChangeTypes + + if ((changeTypes and AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT) != 0 + && (changeTypes + and AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) + == 0 + ) { + return + } + } + + super.sendAccessibilityEventUnchecked(host, event) + } + }) + } + @JvmStatic fun isTablet(context: Context) = when (PrefsUtility.appearance_twopane()) { AppearanceTwopane.AUTO -> context.resources.configuration.screenLayout and diff --git a/src/main/java/org/quantumbadger/redreader/views/RedditCommentView.java b/src/main/java/org/quantumbadger/redreader/views/RedditCommentView.java index f309b124f..ab64fa239 100644 --- a/src/main/java/org/quantumbadger/redreader/views/RedditCommentView.java +++ b/src/main/java/org/quantumbadger/redreader/views/RedditCommentView.java @@ -342,6 +342,7 @@ public RedditCommentView( mIndentView = rootView.findViewById(R.id.view_reddit_comment_indentview); mHeader = rootView.findViewById(R.id.view_reddit_comment_header); + General.suppressAccessibilityTextChangeEvents(mHeader); mBodyHolder = rootView.findViewById(R.id.view_reddit_comment_bodyholder); mIndentedContent = rootView.findViewById(R.id.view_reddit_comment_indented_content); diff --git a/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java b/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java index 8b1452c95..037858472 100644 --- a/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java +++ b/src/main/java/org/quantumbadger/redreader/views/RedditPostHeaderView.java @@ -89,6 +89,7 @@ public RedditPostHeaderView( subtitle.setTextSize(13.0f * subtitleFontScale); subtitle.setText(post.buildSubtitle(activity, true)); subtitle.setContentDescription(post.buildAccessibilitySubtitle(activity, true)); + General.suppressAccessibilityTextChangeEvents(subtitle); subtitle.setTextColor(Color.rgb(200, 200, 200)); greyHeader.addView(subtitle); diff --git a/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java b/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java index 26d021b6b..e4b4bb188 100644 --- a/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java +++ b/src/main/java/org/quantumbadger/redreader/views/RedditPostView.java @@ -228,6 +228,8 @@ public void handleMessage(@NonNull final Message msg) { title = Objects.requireNonNull(rootView.findViewById(R.id.reddit_post_title)); subtitle = Objects.requireNonNull(rootView.findViewById(R.id.reddit_post_subtitle)); + General.suppressAccessibilityTextChangeEvents(subtitle); + mCommentsButtonPref = PrefsUtility.appearance_post_show_comments_button(); From 7b0c1cd92c475938336999b1c336fd63cae3cace Mon Sep 17 00:00:00 2001 From: QuantumBadger Date: Sun, 13 Sep 2026 14:02:18 +0100 Subject: [PATCH 4/6] Add individual comments for announcement strings --- src/main/res/values/strings.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c03368a78..d969cd651 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1956,13 +1956,19 @@ + Upvoted + Downvoted + Removed vote + Collapsed comment + Expanded From b61541450e02b64c266adf7469b0252d789e61d3 Mon Sep 17 00:00:00 2001 From: QuantumBadger Date: Sun, 13 Sep 2026 14:22:00 +0100 Subject: [PATCH 5/6] Updated changelog (#1380) --- src/main/assets/changelog-alpha.txt | 1 + src/main/assets/changelog.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/assets/changelog-alpha.txt b/src/main/assets/changelog-alpha.txt index 994d87fd3..58445a9df 100644 --- a/src/main/assets/changelog-alpha.txt +++ b/src/main/assets/changelog-alpha.txt @@ -1,4 +1,5 @@ /Alpha 370 (2026-09-13) +Accessibility announcements for certain actions (collapsing/expanding comments, and voting) Fixes for inline image preview size in landscape mode Improved memory usage for inline image previews diff --git a/src/main/assets/changelog.txt b/src/main/assets/changelog.txt index 4c7fa2fcc..45ef13e91 100644 --- a/src/main/assets/changelog.txt +++ b/src/main/assets/changelog.txt @@ -1,4 +1,5 @@ 119/1.27 +Accessibility announcements for certain actions (collapsing/expanding comments, and voting) Fixes for inline image preview size in landscape mode Improved memory usage for inline image previews From 951f5d6f0a97018b4701a0e1805043bd46bd3177 Mon Sep 17 00:00:00 2001 From: QuantumBadger Date: Sun, 13 Sep 2026 14:25:18 +0100 Subject: [PATCH 6/6] Changelog fix (#1380) --- src/main/assets/changelog-alpha.txt | 2 +- src/main/assets/changelog.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/assets/changelog-alpha.txt b/src/main/assets/changelog-alpha.txt index 58445a9df..36af6dbee 100644 --- a/src/main/assets/changelog-alpha.txt +++ b/src/main/assets/changelog-alpha.txt @@ -1,5 +1,5 @@ /Alpha 370 (2026-09-13) -Accessibility announcements for certain actions (collapsing/expanding comments, and voting) +Accessibility announcements for collapsing/expanding comments, and voting (thanks to codeofdusk) Fixes for inline image preview size in landscape mode Improved memory usage for inline image previews diff --git a/src/main/assets/changelog.txt b/src/main/assets/changelog.txt index 45ef13e91..23c2ddb98 100644 --- a/src/main/assets/changelog.txt +++ b/src/main/assets/changelog.txt @@ -1,5 +1,5 @@ 119/1.27 -Accessibility announcements for certain actions (collapsing/expanding comments, and voting) +Accessibility announcements for collapsing/expanding comments, and voting (thanks to codeofdusk) Fixes for inline image preview size in landscape mode Improved memory usage for inline image previews