From 2c5ea001d32d674cd33ff1e2064d3bb98a208314 Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:12:48 +0200 Subject: [PATCH 1/2] Add dynamic accessibility labels to app-list Internet toggle The app icon in the main list doubles as a button that blocks/allows Internet access for that app, but had no meaningful contentDescription for screen readers beyond a static, non-state-aware label added years ago. Set a dynamic contentDescription in AdapterRule reflecting the app name and current state ("Block/Allow internet access for X", or a no-op description when the app has no Internet permission), updated both on bind and immediately after each tap. Also label the previously-unlabeled MaterialSwitch toggles in the per-app tracker details screen (TrackersListAdapter / rule.xml), which suffer the same class of issue: the per-category "Block" switch gets a dynamic description naming its category, and the Tracker Protection / Internet Access / Exclude from VPN switches in the header get their existing visible labels as contentDescription. Closes #231 Co-Authored-By: Claude Opus 4.8 --- .../eu/faircode/netguard/AdapterRule.java | 21 ++++++++++++++++++- .../details/TrackersListAdapter.java | 6 +++++- .../res/layout/list_item_trackers_header.xml | 3 +++ app/src/main/res/values/strings.xml | 6 ++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index 0c8917055..a2c638ab9 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -261,7 +261,9 @@ public void onBindViewHolder(final ViewHolder holder, int position) { // Show if Internet access blocked final ImageView iv = holder.ivIcon; InternetBlocklist internetBlocklist = InternetBlocklist.getInstance(context); - setGreyscale(iv, rule.internet && active && internetBlocklist.blockedInternet(rule.uid)); + boolean blockedInternet = internetBlocklist.blockedInternet(rule.uid); + setGreyscale(iv, rule.internet && active && blockedInternet); + updateToggleContentDescription(iv, context, rule, blockedInternet); holder.ivIcon.setOnClickListener(view -> { if (!rule.internet) return; @@ -282,6 +284,7 @@ public void onBindViewHolder(final ViewHolder holder, int position) { Toast.makeText(context, R.string.internet_blocked, Toast.LENGTH_SHORT).show(); } setGreyscale(iv, !wasBlocked); + updateToggleContentDescription(iv, context, rule, !wasBlocked); }); boolean pastWeekOnly = !("trackers_all".equals(cachedSort)); @@ -322,6 +325,22 @@ public void onBindViewHolder(final ViewHolder holder, int position) { // click listeners on hidden views) was the primary cause of scroll jank. } + /** + * Sets a screen-reader-friendly content description on the app icon / + * Internet toggle button, reflecting the app's name and current + * block/allow state (accessibility fix for #231). + */ + private void updateToggleContentDescription(ImageView iv, Context context, Rule rule, boolean blocked) { + final String description; + if (!rule.internet) + description = context.getString(R.string.toggle_no_internet_description, rule.name); + else if (blocked) + description = context.getString(R.string.toggle_allow_internet_description, rule.name); + else + description = context.getString(R.string.toggle_block_internet_description, rule.name); + iv.setContentDescription(description); + } + private void setGreyscale(ImageView iv, boolean on) { if (on) { iv.setColorFilter(GREYSCALE_FILTER); diff --git a/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java b/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java index e537e87fe..4ec7ed8d7 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java +++ b/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java @@ -266,7 +266,11 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder _holder, int posit holder.mUncertain.setVisibility(trackerCategory.isUncertain() ? View.VISIBLE : View.GONE); // Add data to view - holder.mTrackerCategoryName.setText(trackerCategory.getDisplayName(mContext)); + String categoryDisplayName = trackerCategory.getDisplayName(mContext); + holder.mTrackerCategoryName.setText(categoryDisplayName); + holder.mSwitchTracker.setContentDescription( + String.format(mContext.getString(R.string.toggle_block_category_description), + categoryDisplayName)); final ArrayAdapter trackersAdapter = new ArrayAdapter(mContext, R.layout.list_item_trackers_details, trackerCategory.getChildren()) { @Override diff --git a/app/src/main/res/layout/list_item_trackers_header.xml b/app/src/main/res/layout/list_item_trackers_header.xml index 7596b01ce..c39d48206 100644 --- a/app/src/main/res/layout/list_item_trackers_header.xml +++ b/app/src/main/res/layout/list_item_trackers_header.xml @@ -233,6 +233,7 @@ android:layout_height="wrap_content" android:minHeight="0dp" android:textColor="?android:textColorPrimary" + android:contentDescription="@string/vpn_exclude" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/vpn_exclude_name" /> @@ -281,6 +282,7 @@ android:layout_height="wrap_content" android:minHeight="0dp" android:textColor="?android:textColorPrimary" + android:contentDescription="@string/exclude_vpn" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/root_name" /> @@ -327,6 +329,7 @@ android:layout_height="wrap_content" android:minHeight="0dp" android:textColor="?android:textColorPrimary" + android:contentDescription="@string/internet_access" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/root_name2" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8402a02fe..218708732 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -468,6 +468,12 @@ Your internet traffic is not being sent to a remote VPN server. Disable internet access for this application Application Name + + Block internet access for %1$s + Allow internet access for %1$s + %1$s has no internet access + + Block trackers in category %1$s contacted %d company last week contacted %d companies last week From 7b8cd6cb2ba0b4f6833104b59963a7e929ce2ea5 Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:29:17 +0200 Subject: [PATCH 2/2] Fix inactive accessibility toggle label --- .../main/java/eu/faircode/netguard/AdapterRule.java | 10 +++++++--- app/src/main/res/values/strings.xml | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index a2c638ab9..44c317e68 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -263,7 +263,7 @@ public void onBindViewHolder(final ViewHolder holder, int position) { InternetBlocklist internetBlocklist = InternetBlocklist.getInstance(context); boolean blockedInternet = internetBlocklist.blockedInternet(rule.uid); setGreyscale(iv, rule.internet && active && blockedInternet); - updateToggleContentDescription(iv, context, rule, blockedInternet); + updateToggleContentDescription(iv, context, rule, active, blockedInternet); holder.ivIcon.setOnClickListener(view -> { if (!rule.internet) return; @@ -284,7 +284,7 @@ public void onBindViewHolder(final ViewHolder holder, int position) { Toast.makeText(context, R.string.internet_blocked, Toast.LENGTH_SHORT).show(); } setGreyscale(iv, !wasBlocked); - updateToggleContentDescription(iv, context, rule, !wasBlocked); + updateToggleContentDescription(iv, context, rule, true, !wasBlocked); }); boolean pastWeekOnly = !("trackers_all".equals(cachedSort)); @@ -330,10 +330,14 @@ public void onBindViewHolder(final ViewHolder holder, int position) { * Internet toggle button, reflecting the app's name and current * block/allow state (accessibility fix for #231). */ - private void updateToggleContentDescription(ImageView iv, Context context, Rule rule, boolean blocked) { + private void updateToggleContentDescription(ImageView iv, Context context, Rule rule, + boolean active, boolean blocked) { final String description; if (!rule.internet) description = context.getString(R.string.toggle_no_internet_description, rule.name); + else if (!active) + description = context.getString(R.string.toggle_internet_unavailable_description, + rule.name); else if (blocked) description = context.getString(R.string.toggle_allow_internet_description, rule.name); else diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 218708732..c4cd5d7a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -472,8 +472,9 @@ Your internet traffic is not being sent to a remote VPN server. Block internet access for %1$s Allow internet access for %1$s %1$s has no internet access + Internet access control unavailable for %1$s - Block trackers in category %1$s + Tracker blocking for category %1$s contacted %d company last week contacted %d companies last week