critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

ScreenReaderDetectionService implements WidgetsBindingObserver and registers/unregisters itself correctly
Stream<bool> isScreenReaderActive emits the current state immediately upon subscription
Stream emits true when VoiceOver (iOS) or TalkBack (Android) is active
Stream emits false when neither VoiceOver nor TalkBack is active
Stream re-evaluates and emits updated value when app returns to foreground (didChangeAppLifecycleState → resumed)
Stream re-evaluates when system accessibility features change (didChangeAccessibilityFeatures callback)
Service is properly disposed: WidgetsBinding observer removed, StreamController closed, no resource leaks
Service is registered as a Riverpod StreamProvider<bool> (named screenReaderActiveProvider or similar)
accessibleNavigation detection works on both iOS (VoiceOver) and Android (TalkBack) without platform-specific code branches in the public interface
If the screen reader state cannot be determined (e.g. during testing), service emits false as a safe default
Manual override from AccessibilitySettings.screenReaderOverride=true forces the stream to emit true regardless of system state

Technical Requirements

frameworks
Flutter
Riverpod
apis
WidgetsBindingObserver
MediaQueryData.accessibleNavigation
AccessibilityFeatures
WidgetsBinding.instance
data models
AccessibilitySettings
performance requirements
Detection on app foreground must complete and emit within one frame (16ms)
Stream must not emit duplicate consecutive values (deduplicate with distinctValues or manual check)
security requirements
Detection result must not be cached to disk — it is a runtime platform state
Service must not request any additional permissions beyond what Flutter accessibility APIs provide

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use a StreamController.broadcast() internally. Implement didChangeAccessibilityFeatures() from WidgetsBindingObserver — this is called on both iOS and Android when accessibility settings change. Supplement with didChangeAppLifecycleState() to catch cases where the user toggles accessibility while the app is backgrounded. To read the current state: use WidgetsBinding.instance.accessibilityFeatures.accessibleNavigation (available without a BuildContext).

Note MediaQuery.accessibleNavigation requires a BuildContext and cannot be used outside of widget tree — prefer WidgetsBinding.instance.accessibilityFeatures for service-layer detection. Integrate with AccessibilitySettingsRepository to read the screenReaderOverride flag — if override is true, emit true unconditionally. Deduplicate stream emissions to avoid causing redundant UI rebuilds: compare new value with last emitted before adding to the controller.

Testing Requirements

Write unit tests using flutter_test: (1) stream emits false by default (no screen reader active in test environment), (2) manual override forces stream to emit true, (3) didChangeAccessibilityFeatures triggers re-evaluation, (4) didChangeAppLifecycleState(resumed) triggers re-evaluation, (5) dispose removes observer and closes stream without error, (6) stream does not emit duplicate consecutive equal values. Use TestWidgetsFlutterBinding and mockMediaQuery to simulate accessibility state. Integration test on a device/simulator should verify real VoiceOver/TalkBack detection — tag as @Tags(['integration']). Minimum 80% branch coverage on the service.

Component
Screen Reader Detection Service
service medium
Epic Risks (2)
high impact medium prob technical

Flutter's SemanticsService behaves differently between iOS (VoiceOver) and Android (TalkBack) in edge cases — e.g., announcement queuing, focus-gain timing, and attribute support. If the facade does not correctly abstract these differences, announcements may be silent or misfired on one platform, causing regression on the other platform to go unnoticed until device testing.

Mitigation & Contingency

Mitigation: Write platform-divergence unit tests early using SemanticsServiceFacade mocks. Validate announcement delivery on a physical iPhone (VoiceOver) and Android device (TalkBack) at the end of each sprint. Document known platform differences in the facade's inline API comments.

Contingency: If a platform difference cannot be abstracted cleanly, expose a platform-specific override path in the facade and implement targeted workarounds per platform, accepting the added complexity in exchange for correct behaviour.

medium impact medium prob scope

Accessibility preferences stored in local storage may need new fields as higher-tier epics are implemented (e.g., announcement verbosity, sensitive-field guard toggle). Schema changes to an already-persisted store risk data migration failures or silent defaults on existing installs, breaking user preferences.

Mitigation & Contingency

Mitigation: Design the AccessibilitySettingsRepository with a versioned JSON schema from the start, using merge-with-defaults on read so new fields fall back gracefully. Define the full expected field list upfront based on all downstream epic requirements before writing the first record.

Contingency: If migration fails on a live install, fall back to full reset-to-defaults with a one-time in-app notification informing the user that accessibility preferences have been reset and inviting them to reconfigure.