Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,7 @@
## 2026-05-30 - User-Facing System Notifications
**Learning:** Foreground service notifications and channels are exposed directly to users within their system drawer and app settings. Hardcoding technical or aggressive terminology (e.g., naming a channel ID as the visible name or using "Kill" as an action button) degrades the user experience and violates UX standards. Furthermore, failing to extract these strings to `strings.xml` prevents localization and accessibility optimizations.
**Action:** Always avoid hardcoded, technical, or aggressive terminology in user-facing system notifications and foreground service controls. Utilize standard mobile UX phrasing (e.g., "Stop", "Tap") and ensure all notification text (titles, descriptions, actions) is extracted to localized string resources.

## 2026-06-08 - Async View Initialization and Accessibility
**Learning:** Interactive elements with dynamic text and secondary actions must have their full actionable `contentDescription` and their custom `AccessibilityDelegateCompat` set during view initialization. Waiting for an asynchronous block (like a network request or local IP resolution) to complete before assigning these properties creates a window where the element is completely inaccessible to screen readers, and tapping it may result in no feedback.
**Action:** Always provide explicit feedback (e.g., a Toast) if a user interacts with a UI element that is visibly actionable but functionally disabled due to a loading state. Furthermore, attach the custom `AccessibilityDelegateCompat` to the view immediately during initialization, ensuring screen readers understand its interactive role even during loading.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ class CameraFragment : Fragment() {
): View {
_fragmentCameraBinding = FragmentCameraBinding.inflate(inflater, container, false)

_fragmentCameraBinding?.let { binding ->
// Set initial state before async operation completes
binding.textView6.contentDescription = getString(R.string.actionable_content_description_format, getString(R.string.default_ip), getString(R.string.copy_ip_tooltip))
binding.textView6.setOnClickListener {
Toast.makeText(context, R.string.ip_loading_message, Toast.LENGTH_SHORT).show()
}

ViewCompat.setAccessibilityDelegate(binding.textView6, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.className = android.widget.Button::class.java.name
val clickAction = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK.id,
getString(R.string.copy_ip_tooltip)
)
info.addAction(clickAction)
}
})

ViewCompat.setAccessibilityDelegate(binding.textView2, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.className = android.widget.Button::class.java.name
val clickAction = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK.id,
getString(R.string.repo_desc)
)
info.addAction(clickAction)
}
})
}

// Get the local ip address
viewLifecycleOwner.lifecycleScope.launch {
val localIp = IpUtil.getLocalIpAddress()
Expand All @@ -102,29 +134,7 @@ class CameraFragment : Fragment() {
}
}

ViewCompat.setAccessibilityDelegate(binding.textView6, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.className = android.widget.Button::class.java.name
val clickAction = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK.id,
getString(R.string.copy_ip_tooltip)
)
info.addAction(clickAction)
}
})

ViewCompat.setAccessibilityDelegate(binding.textView2, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.className = android.widget.Button::class.java.name
val clickAction = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK.id,
getString(R.string.repo_desc)
)
info.addAction(clickAction)
}
})

binding.textView2.setOnClickListener {
val url = getString(R.string.repo_url)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,5 @@
<string name="frame_time_format">%1$d ms</string>
<string name="bitrate_format">%1$d kB/sec</string>
<string name="quality_format">%1$d%%</string>
<string name="ip_loading_message">Loading IP address, please wait&#8230;</string>
</resources>