Skip to content

Fix multiple Android app issues#138

Open
gfauredev wants to merge 3 commits intomainfrom
copilot/fix-android-app-issues
Open

Fix multiple Android app issues#138
gfauredev wants to merge 3 commits intomainfrom
copilot/fix-android-app-issues

Conversation

@gfauredev
Copy link
Copy Markdown
Owner

@gfauredev gfauredev commented Apr 9, 2026

This Pull Request…

Engineering Principles

  • PR only contains changes strictly related to the requested feature or fix,
    scope is focused (no unrelated dependency updates or formatting)
  • This code totally respects README’s Engineering Principles

CI/CD Readiness

  • Branch follows Conventional Branch: feat/…, fix/…, refactor/…, …
  • Code is formatted with dx fmt; cargo fmt
  • All checks pass, nix flake checks succeeds without warnings
    • Code compiles, dx build with necessary platform flags succeeds
    • cargo clippy -- -D warnings -W clippy::all -W clippy::pedantic
      produces zero warnings
    • All unit tests pass without warnings
      cargo llvm-cov nextest --ignore-filename-regex '(src/components/|\.cargo/registry/|nix/store)'
    • End-to-end tests pass maestro test --headless maestro/web
      maestro test --headless maestro/android

Summary by CodeRabbit

  • New Features

    • Enabled push notifications for rest and exercise duration alerts on Android devices.
    • Enhanced Android keyboard handling for improved app responsiveness.
  • Bug Fixes

    • Improved note synchronisation stability when switching between sessions.
    • Resolved Android WebView caching of error responses.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Changes encompass Android platform enablement through configuration updates, a new MainActivity implementation managing WebView lifecycle synchronization, refactored session note synchronization logic based on session ID changes, batch completion tracking for image downloads, and cache control headers for error responses on mobile platforms.

Changes

Cohort / File(s) Summary
Android Configuration & Activity
Dioxus.toml, android/MainActivity.kt
Uncommented Android MainActivity reference and activated POST_NOTIFICATIONS permission in configuration. Implemented new MainActivity subclassing WryActivity with lifecycle overrides that synchronise WebView resume/pause events and handle soft keyboard layout adjustments.
Session State Management
src/components/active_session/mod.rs
Refactored notes textarea synchronisation from external session change tracking to session ID-based control flow. Added last_synced_session_id signal and conditional DOM updates to prevent cursor resets during debounce-driven saves whilst maintaining proper synchronisation across session switches.
Image Download Tracking
src/components/exercise_card.rs
Added batch completion counter to ExerciseImage that increments when image download progress context clears. Updated memo subscription to re-evaluate on batch completion in addition to image index changes on non-wasm32 platforms.
Cache Control
src/services/imgcache.rs
Added Cache-Control: no-store HTTP header to all error responses on mobile platforms to prevent WebView caching of "not found" results within the same session.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

A rabbit hops through Android paths,
With MainActivity at the helm,
WebView symphonies in lifecycle baths,
Sessions sync without a qualm—
Cache-free notes and batches complete,
The app now bounces to a faster beat! 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description uses the required template structure with Engineering Principles and CI/CD Readiness sections; however, most CI/CD readiness checks remain unchecked, and no narrative explanation is provided about what issues are being fixed. Add a brief narrative explaining which Android issues are being fixed and why (notifications, lifecycle, notes sync, caching). Verify and complete CI/CD readiness checks before merging.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix multiple Android app issues' is concise and accurately reflects the main changes: addressing multiple Android-specific problems including notifications, lifecycle handling, and caching.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch copilot/fix-android-app-issues

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

📊 Coverage Report

Lines: 3754/5005 (75.004995004995%)

⏱️ Tests: 255 tests in 0.662s

FilenameFunction CoverageLine CoverageRegion CoverageBranch Coverage
main.rs
  22.00% (11/50)
  58.94% (155/263)
  60.56% (218/360)
- (0/0)
models/analytics.rs
   0.00% (0/6)
   0.00% (0/34)
   0.00% (0/46)
- (0/0)
models/enums.rs
 100.00% (28/28)
 100.00% (147/147)
 100.00% (337/337)
- (0/0)
models/exercise.rs
  92.00% (46/50)
  92.10% (548/595)
  91.01% (749/823)
- (0/0)
models/log.rs
 100.00% (12/12)
 100.00% (118/118)
 100.00% (144/144)
- (0/0)
models/mod.rs
 100.00% (11/11)
 100.00% (68/68)
 100.00% (98/98)
- (0/0)
models/session.rs
  72.22% (13/18)
  84.36% (151/179)
  83.33% (210/252)
- (0/0)
models/units.rs
 100.00% (28/28)
 100.00% (167/167)
  98.88% (353/357)
- (0/0)
services/app_state.rs
   1.89% (1/53)
   2.54% (11/433)
   2.32% (15/646)
- (0/0)
services/exercise_db.rs
  88.81% (127/143)
  90.30% (1210/1340)
  89.61% (1924/2147)
- (0/0)
services/exercise_loader.rs
   0.00% (0/14)
   0.00% (0/100)
   0.00% (0/135)
- (0/0)
services/native_queue.rs
   0.00% (0/15)
   0.00% (0/145)
   0.00% (0/197)
- (0/0)
services/notifications.rs
   0.00% (0/3)
   0.00% (0/9)
   0.00% (0/10)
- (0/0)
services/service_worker.rs
 100.00% (2/2)
 100.00% (6/6)
 100.00% (6/6)
- (0/0)
services/storage.rs
  63.77% (88/138)
  80.97% (766/946)
  83.53% (1187/1421)
- (0/0)
services/wake_lock.rs
 100.00% (2/2)
 100.00% (5/5)
 100.00% (5/5)
- (0/0)
utils.rs
  89.61% (69/77)
  89.33% (402/450)
  90.14% (640/710)
- (0/0)
Totals
  67.38% (438/650)
  75.00% (3754/5005)
  76.50% (5886/7694)
- (0/0)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/active_session/mod.rs`:
- Around line 132-170: The one-off textarea initialisation currently uses
use_hook (variable initial_notes) but should run in a post-render effect; remove
the use_hook block and instead add a use_effect that runs once after mount to
set the DOM value of the textarea (element id "session-notes-input") when
initial_notes is not empty, using the same serde_json::to_string and
document::eval pattern; keep the existing session-ID logic with
last_synced_session_id and notes_input intact so the existing use_effect for
session changes still distinguishes between same-session vs different-session
updates and avoids cursor resets.

In `@src/components/exercise_card.rs`:
- Around line 43-76: The effect that bumps completed_batches currently
increments on mount if the global progress signal is already None; change the
use_effect for ImageDownloadProgressSignal so it tracks the previous progress
value and only increments completed_batches when the progress transitions from
Some(...) to None (i.e. prev.is_some() && current.is_none()); update the tracked
previous value inside the same effect after checking so future transitions are
detected correctly. Ensure you reference the existing progress
(use_context::<crate::ImageDownloadProgressSignal>().0), completed_batches, and
the use_effect closure when applying the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bc777f51-c44f-4605-92b4-6a19b0da3613

📥 Commits

Reviewing files that changed from the base of the PR and between 14b41c3 and 613c824.

📒 Files selected for processing (5)
  • Dioxus.toml
  • android/MainActivity.kt
  • src/components/active_session/mod.rs
  • src/components/exercise_card.rs
  • src/services/imgcache.rs

Comment on lines +132 to 170
// Initialise the uncontrolled textarea on first mount. Because we never
// bind `value:` to the textarea, the DOM starts blank even when the
// session already has notes (e.g. after reopening the app). This hook
// fires once and sets the initial DOM value via JavaScript.
let initial_notes = session.peek().notes.clone();
use_hook(move || {
if !initial_notes.is_empty() {
let val_js = serde_json::to_string(&initial_notes).unwrap_or_default();
spawn(async move {
document::eval(&format!(
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
));
});
}
});
// Track the session ID so we can distinguish between:
// (a) the debounce saving the user's own input for the *same* session
// → do NOT touch the DOM (would reset cursor on Android)
// (b) a *different* session being loaded
// → update both the signal and the DOM
let mut last_synced_session_id = use_signal(|| session.read().id.clone());
use_effect(move || {
let session_notes = session.read().notes.clone();
if session_notes != *notes_input.peek() {
notes_input.set(session_notes.clone());
// Update the textarea value via JavaScript so the DOM is updated
// without a full re-render. A full re-render would reset the
// cursor to the end of the text on Android's WebView.
let s = session.read();
let new_id = s.id.clone();
let new_notes = s.notes.clone();
if new_id != *last_synced_session_id.peek() {
// Different session loaded – update signal and DOM.
last_synced_session_id.set(new_id);
notes_input.set(new_notes.clone());
spawn(async move {
let val_js = serde_json::to_string(&session_notes).unwrap_or_default();
let val_js = serde_json::to_string(&new_notes).unwrap_or_default();
document::eval(&format!(
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
));
});
}
// Same session: notes changed because the debounce saved the user's
// own input. Leave the DOM alone to avoid resetting the cursor.
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use use_effect for the one-off textarea initialisation.

Dioxus documents use_hook as hook-state initialisation, while direct DOM reads and writes belong in use_effect, which runs after the UI has been rendered. The session-ID gate below is a sensible way to avoid cursor jumps, but the initial fill should move into an effect so it uses the post-render lifecycle instead. (dioxuslabs.com)

♻️ Suggested change
     let initial_notes = session.peek().notes.clone();
-    use_hook(move || {
+    use_effect(move || {
         if !initial_notes.is_empty() {
             let val_js = serde_json::to_string(&initial_notes).unwrap_or_default();
             spawn(async move {
                 document::eval(&format!(
                     "var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
                 ));
             });
         }
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Initialise the uncontrolled textarea on first mount. Because we never
// bind `value:` to the textarea, the DOM starts blank even when the
// session already has notes (e.g. after reopening the app). This hook
// fires once and sets the initial DOM value via JavaScript.
let initial_notes = session.peek().notes.clone();
use_hook(move || {
if !initial_notes.is_empty() {
let val_js = serde_json::to_string(&initial_notes).unwrap_or_default();
spawn(async move {
document::eval(&format!(
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
));
});
}
});
// Track the session ID so we can distinguish between:
// (a) the debounce saving the user's own input for the *same* session
// → do NOT touch the DOM (would reset cursor on Android)
// (b) a *different* session being loaded
// → update both the signal and the DOM
let mut last_synced_session_id = use_signal(|| session.read().id.clone());
use_effect(move || {
let session_notes = session.read().notes.clone();
if session_notes != *notes_input.peek() {
notes_input.set(session_notes.clone());
// Update the textarea value via JavaScript so the DOM is updated
// without a full re-render. A full re-render would reset the
// cursor to the end of the text on Android's WebView.
let s = session.read();
let new_id = s.id.clone();
let new_notes = s.notes.clone();
if new_id != *last_synced_session_id.peek() {
// Different session loaded – update signal and DOM.
last_synced_session_id.set(new_id);
notes_input.set(new_notes.clone());
spawn(async move {
let val_js = serde_json::to_string(&session_notes).unwrap_or_default();
let val_js = serde_json::to_string(&new_notes).unwrap_or_default();
document::eval(&format!(
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
));
});
}
// Same session: notes changed because the debounce saved the user's
// own input. Leave the DOM alone to avoid resetting the cursor.
});
// Initialise the uncontrolled textarea on first mount. Because we never
// bind `value:` to the textarea, the DOM starts blank even when the
// session already has notes (e.g. after reopening the app). This hook
// fires once and sets the initial DOM value via JavaScript.
let initial_notes = session.peek().notes.clone();
use_effect(move || {
if !initial_notes.is_empty() {
let val_js = serde_json::to_string(&initial_notes).unwrap_or_default();
spawn(async move {
document::eval(&format!(
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
));
});
}
});
// Track the session ID so we can distinguish between:
// (a) the debounce saving the user's own input for the *same* session
// → do NOT touch the DOM (would reset cursor on Android)
// (b) a *different* session being loaded
// → update both the signal and the DOM
let mut last_synced_session_id = use_signal(|| session.read().id.clone());
use_effect(move || {
let s = session.read();
let new_id = s.id.clone();
let new_notes = s.notes.clone();
if new_id != *last_synced_session_id.peek() {
// Different session loaded – update signal and DOM.
last_synced_session_id.set(new_id);
notes_input.set(new_notes.clone());
spawn(async move {
let val_js = serde_json::to_string(&new_notes).unwrap_or_default();
document::eval(&format!(
"var el=document.getElementById('session-notes-input');if(el)el.value={val_js};"
));
});
}
// Same session: notes changed because the debounce saved the user's
// own input. Leave the DOM alone to avoid resetting the cursor.
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/active_session/mod.rs` around lines 132 - 170, The one-off
textarea initialisation currently uses use_hook (variable initial_notes) but
should run in a post-render effect; remove the use_hook block and instead add a
use_effect that runs once after mount to set the DOM value of the textarea
(element id "session-notes-input") when initial_notes is not empty, using the
same serde_json::to_string and document::eval pattern; keep the existing
session-ID logic with last_synced_session_id and notes_input intact so the
existing use_effect for session changes still distinguishes between same-session
vs different-session updates and avoids cursor resets.

Comment on lines +43 to +76
// Number of downloads completed so far in this session. Incremented
// whenever the progress signal transitions from Some(...) to None (i.e.
// the download batch finishes) so the URL memo re-evaluates and picks up
// newly-cached images without flickering on every individual file.
#[cfg(not(target_arch = "wasm32"))]
let mut completed_batches: Signal<u32> = use_signal(|| 0);
#[cfg(not(target_arch = "wasm32"))]
{
let progress = use_context::<crate::ImageDownloadProgressSignal>().0;
use_effect(move || {
let current = *progress.read();
if current.is_none() {
// A batch just finished – bump the counter so sync_url
// re-evaluates and can switch to the imgcache:// URL.
let prev = *completed_batches.peek();
completed_batches.set(prev + 1);
}
});
}

// Synchronous URL via the shared model method (covers all non-idb: keys).
// Only re-evaluated when the user cycles through images (img_index changes).
// Deliberately does NOT subscribe to the download-progress signal: switching
// the src from a remote URL to an imgcache:// URL mid-display causes the
// Android WebView to blank the image briefly while the custom protocol handler
// serves the new request. Images cached during this session are picked up on
// the next component mount (e.g. after scrolling or the next app launch).
// Re-evaluated when the user cycles images OR when a download batch ends.
let sync_url = {
let ex = exercise.clone();
use_memo(move || ex.get_image_url(*img_index.read()))
#[cfg(not(target_arch = "wasm32"))]
{
use_memo(move || {
let _batch = *completed_batches.read(); // subscribe to batch completions
ex.get_image_url(*img_index.read())
})
}
#[cfg(target_arch = "wasm32")]
{
use_memo(move || ex.get_image_url(*img_index.read()))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Only bump completed_batches on a real Some -> None transition.

This effect increments the counter on first mount whenever the global progress signal is already None, so every card does one needless recompute before any download batch has actually finished. Track the previous download state and only increment on an actual completion transition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/exercise_card.rs` around lines 43 - 76, The effect that bumps
completed_batches currently increments on mount if the global progress signal is
already None; change the use_effect for ImageDownloadProgressSignal so it tracks
the previous progress value and only increments completed_batches when the
progress transitions from Some(...) to None (i.e. prev.is_some() &&
current.is_none()); update the tracked previous value inside the same effect
after checking so future transitions are detected correctly. Ensure you
reference the existing progress
(use_context::<crate::ImageDownloadProgressSignal>().0), completed_batches, and
the use_effect closure when applying the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants