feat: 러닝 화면에 일시정지/재개 기능 추가 - #406
Conversation
기존엔 러닝 종료 버튼 하나만 있어 중간에 멈출 수 없었음. 기존 "러닝 종료" 풀와이드 버튼을 원형 일시정지(주) 버튼으로 교체하고, 일시정지 시에만 종료(정지)/재개 버튼 두 개가 나타나도록 했다. 지도는 NRC처럼 별도 화면으로 바꾸지 않고 그대로 유지하되, 옅은 틴트와 "일시정지 중" 배지로 상태를 표시해 코스 경로를 계속 보여준다. TimerService에 pauseTimer/resumeTimer를 추가했는데, 기존 time 값을 초기화하지 않는 startTimer/stopTimer 구조를 그대로 재사용해 일시정지 후 재개하면 멈췄던 지점부터 이어서 카운트된다.
📝 WalkthroughWalkthroughThe run screen now supports pause and resume actions. TimerService preserves elapsed time during pauses. The UI displays paused state and uses a stop control to complete the run. ChangesRun pause flow
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Runner
participant RunActivity
participant TimerService
participant RunViewModel
Runner->>RunActivity: Tap pause or resume
RunActivity->>TimerService: Pause or resume timer
RunActivity->>RunViewModel: Update isPaused
RunActivity->>Runner: Update controls and paused label
Runner->>RunActivity: Tap stop
RunActivity->>TimerService: Stop timer
RunActivity->>Runner: Navigate to run completion
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt (1)
364-384: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick winInitialize
timerDatabefore stop can use it.
timerDataislateinitand is only assigned bytimerReceiver, but the stop path reads it immediately after callingstopTimer(). If stop runs before the first timer broadcast,RunToEndRunDataconstruction accesses an uninitialized property. InitializetimerDatato zero time before enabling stop, or pass the current elapsed time fromTimerServicewhen stopping.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt` around lines 364 - 384, Initialize the lateinit timerData state to a zero-valued timer before the stop button can be used, or have stopTimer return the current elapsed time from TimerService and use it in the stop handler. Ensure the btnRunStop listener can safely construct RunToEndRunData even when timerReceiver has not yet delivered a broadcast.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt`:
- Line 100: Update RunActivity.onCreate() to read the retained
RunViewModel.isPaused value and call updatePauseResumeUI() with it before
setUpPauseResume(), ensuring the recreated activity displays the correct
pause/resume state.
- Around line 137-146: In setUpPauseResume, return immediately from the click
listener when timerService is null, before invoking pause/resume or updating
viewModel.isPaused and the UI. Only perform the existing toggle and
updatePauseResumeUI flow after confirming the service is bound.
---
Outside diff comments:
In `@app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt`:
- Around line 364-384: Initialize the lateinit timerData state to a zero-valued
timer before the stop button can be used, or have stopTimer return the current
elapsed time from TimerService and use it in the stop handler. Ensure the
btnRunStop listener can safely construct RunToEndRunData even when timerReceiver
has not yet delivered a broadcast.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 238b5d96-4be4-4e50-8d6e-e5d1a9489214
📒 Files selected for processing (11)
app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.ktapp/src/main/java/com/runnect/runnect/presentation/run/RunViewModel.ktapp/src/main/java/com/runnect/runnect/presentation/run/TimerService.ktapp/src/main/res/drawable/bg_paused_pill.xmlapp/src/main/res/drawable/circle_ghost_button.xmlapp/src/main/res/drawable/circle_m1_button.xmlapp/src/main/res/drawable/ic_run_pause.xmlapp/src/main/res/drawable/ic_run_resume.xmlapp/src/main/res/drawable/ic_run_stop.xmlapp/src/main/res/layout/activity_run.xmlapp/src/main/res/values/strings.xml
| getCurrentLocation() | ||
| showRecord() | ||
| backButton() | ||
| setUpPauseResume() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate RunActivity.kt =="
fd -a 'RunActivity\.kt$' . || true
echo "== file outline =="
if [ -f app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt ]; then
ast-grep outline app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt --view expanded || true
echo "== relevant lines =="
sed -n '1,220p' app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt | nl -ba -v1
fi
echo "== search RunViewModel isPaused/updatePauseResumeUI == "
rg -n "isPaused|updatePauseResumeUI|setUpPauseResume|ViewModel|onResume|onPause|onStop" app/src/main/java/com/runnect/runnect/presentation/run -S || trueRepository: Runnect/Runnect-Android
Length of output: 3001
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== RunActivity.kt relevant sections =="
sed -n '80,175p' app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt
echo "== RunViewModel references =="
rg -n "class RunViewModel|RunViewModel|isPaused|updatePauseResumeUI|setUpPauseResume" app/src/main/java/com/runnect/runnect/presentation/run -S
echo "== TimerService pause references =="
rg -n "pause|resume|isPaused|broadcast|Pause|Resume" app/src/main/java/com/runnect/runnect -S || trueRepository: Runnect/Runnect-Android
Length of output: 8933
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== RunViewModel.kt =="
sed -n '1,80p' app/src/main/java/com/runnect/runnect/presentation/run/RunViewModel.kt
echo "== timerService pause/resume interaction in TimerService and TimerHandler =="
rg -n "mHandler|mTimerTask|timerService|pause|resume|stop|start" app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt -S
sed -n '1,160p' app/src/main/java/com/runnect/runnect/presentation/run/TimerService.ktRepository: Runnect/Runnect-Android
Length of output: 6404
Render the retained pause state when the activity is recreated.
RunViewModel.isPaused survives configuration changes, but updatePauseResumeUI() only runs from the pause/resume button callback. After rotation during a pause, the recreated UI shows the running icon while the timer service remains paused.
Read isPaused in onCreate() from the retained ViewModel value and call updatePauseResumeUI() for that value before setting up the button listener.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt` at
line 100, Update RunActivity.onCreate() to read the retained
RunViewModel.isPaused value and call updatePauseResumeUI() with it before
setUpPauseResume(), ensuring the recreated activity displays the correct
pause/resume state.
| private fun setUpPauseResume() { | ||
| binding.btnRunPauseResume.setOnClickListener { | ||
| val isPaused = viewModel.isPaused.value ?: false | ||
| if (isPaused) { | ||
| timerService?.resumeTimer() | ||
| } else { | ||
| timerService?.pauseTimer() | ||
| } | ||
| viewModel.isPaused.value = !isPaused | ||
| updatePauseResumeUI(!isPaused) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
fd -a 'RunActivity\.kt$|TimerService\.kt$' . | sed 's#^\./##'
echo "== RunActivity outline/sections =="
RUNACTIVITY="$(fd 'RunActivity\.kt$' . | head -n1 || true)"
if [ -n "${RUNACTIVITY:-}" ]; then
echo "--- $RUNACTIVITY lines 1-230 ---"
sed -n '1,230p' "$RUNACTIVITY" | nl -ba
fi
echo "== TimerService outline/sections =="
TIMER="$(fd 'TimerService\.kt$' . | head -n1 || true)"
if [ -n "${TIMER:-}" ]; then
echo "--- $TIMER lines 1-260 ---"
sed -n '1,260p' "$TIMER" | nl -ba
fi
echo "== references to timerService/isPaused/pauseResume/updatePauseResumeUI =="
rg -n "timerService|isPaused|pauseResume|updatePauseResumeUI|onServiceConnected|bindService|UnbindService|btnRunPauseResume" app/src/main/java || trueRepository: Runnect/Runnect-Android
Length of output: 628
🏁 Script executed:
#!/bin/bash
set -euo pipefail
RUNACTIVITY="$(fd 'RunActivity\.kt$' . | grep 'app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt' || true)"
TIMER="$(fd 'TimerService\.kt$' . | grep 'app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt' || true)"
echo "== RunActivity relevant sections =="
sed -n '1,240p' "${RUNACTIVITY:-/dev/null}"
echo
echo "== TimerService relevant sections =="
sed -n '1,280p' "${TIMER:-/dev/null}"
echo
echo "== references =="
rg -n "timerService|isPaused|setUpPauseResume|updatePauseResumeUI|onServiceConnected|bindService|bindServiceAsync|UnbindService|btnRunPauseResume|pauseTimer|resumeTimer" app/src/main/java || true
echo
echo "== Kotlin safe-call behavior probe =="
node - <<'JS'
let timerServiceCalled = false;
class TimerService {
pauseTimer() {
timerServiceCalled = true;
console.log("pauseTimer called");
}
resumeTimer() {
console.log("resumeTimer called");
}
}
let timerService = null;
let viewModelIsPaused = false;
// Kotlin safe-call null behavior: if receiver is null, expression returns null and body is skipped.
try {
if (timerService != null) {
timerService.pauseTimer();
}
} catch (e) {
console.error(e);
}
viewModelIsPaused = !viewModelIsPaused;
console.log({timerServiceCalled, viewModelIsPaused});
timerService = new TimerService();
if (timerService != null) {
timerService.resumeTimer();
}
console.log({timerServiceCalled});
JSRepository: Runnect/Runnect-Android
Length of output: 17308
Return before updating pause state if timerService is not bound.
timerService is set in onServiceConnected, but the pause button is available immediately. A tap before binding calls resumeTimer() when the service has not bound, then sets isPaused to true. The next tap calls startTimer() while the existing timer is still active, creating a second schedule and doubling elapsed-time broadcasts.
Check timerService before changing either state or UI.
Proposed fix
private fun setUpPauseResume() {
binding.btnRunPauseResume.setOnClickListener {
+ val service = timerService ?: return@setOnClickListener
val isPaused = viewModel.isPaused.value ?: false
if (isPaused) {
- timerService?.resumeTimer()
+ service.resumeTimer()
} else {
- timerService?.pauseTimer()
+ service.pauseTimer()
}
viewModel.isPaused.value = !isPaused
updatePauseResumeUI(!isPaused)
}
}📝 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.
| private fun setUpPauseResume() { | |
| binding.btnRunPauseResume.setOnClickListener { | |
| val isPaused = viewModel.isPaused.value ?: false | |
| if (isPaused) { | |
| timerService?.resumeTimer() | |
| } else { | |
| timerService?.pauseTimer() | |
| } | |
| viewModel.isPaused.value = !isPaused | |
| updatePauseResumeUI(!isPaused) | |
| private fun setUpPauseResume() { | |
| binding.btnRunPauseResume.setOnClickListener { | |
| val service = timerService ?: return@setOnClickListener | |
| val isPaused = viewModel.isPaused.value ?: false | |
| if (isPaused) { | |
| service.resumeTimer() | |
| } else { | |
| service.pauseTimer() | |
| } | |
| viewModel.isPaused.value = !isPaused | |
| updatePauseResumeUI(!isPaused) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt` around
lines 137 - 146, In setUpPauseResume, return immediately from the click listener
when timerService is null, before invoking pause/resume or updating
viewModel.isPaused and the UI. Only perform the existing toggle and
updatePauseResumeUI flow after confirming the service is bound.
작업 배경
변경 사항
activity_run.xmlRunViewModel.ktisPaused상태 추가TimerService.ktpauseTimer()/resumeTimer()추가 — 기존time값을 초기화하지 않는startTimer/stopTimer구조를 그대로 재사용해 재개 시 멈춘 지점부터 이어서 카운트RunActivity.kt영향 범위
showRecord())는 로직 변경 없이 새 종료 버튼(btn_run_stop)에 그대로 재연결 — 종료 시EndRunActivity로 이동하는 기존 동작은 동일.colors.xml/activity_run.xml토큰만 사용, 신규 컬러 없음)Test Plan
./gradlew :app:compileDebugKotlin,:app:assembleDebug로컬 빌드 성공 확인🤖 Generated with Claude Code
Summary by CodeRabbit