Skip to content

feat: 러닝 화면에 일시정지/재개 기능 추가 - #406

Open
unam98 wants to merge 1 commit into
developfrom
feature/run-pause-resume
Open

feat: 러닝 화면에 일시정지/재개 기능 추가#406
unam98 wants to merge 1 commit into
developfrom
feature/run-pause-resume

Conversation

@unam98

@unam98 unam98 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

작업 배경

  • 지금까지 러닝 화면엔 "러닝 종료" 버튼 하나만 있어 러닝 도중 잠시 멈출 방법이 없었음 (NRC 등 다른 러닝 앱엔 있는 기본 기능).

변경 사항

영역 내용
activity_run.xml 풀와이드 "러닝 종료" 버튼 제거 → 원형 일시정지/재개 버튼(항상 노출) + 원형 종료 버튼(일시정지 중에만 노출)으로 교체, 상단에 "일시정지 중" 배지 추가
RunViewModel.kt isPaused 상태 추가
TimerService.kt pauseTimer()/resumeTimer() 추가 — 기존 time 값을 초기화하지 않는 startTimer/stopTimer 구조를 그대로 재사용해 재개 시 멈춘 지점부터 이어서 카운트
RunActivity.kt 일시정지/재개/종료 버튼 클릭 처리, 상태에 따른 버튼 노출 전환
신규 drawable 6개 원형 버튼 배경 2종(M1 필/고스트), 아이콘 3종(일시정지/재개/정지), 일시정지 배지 배경

영향 범위

  • 지도는 NRC처럼 별도 통계 화면으로 바꾸지 않고 그대로 유지 — Runnect는 "코스를 그리는" 서비스라 지금까지 그린 경로를 계속 보여주는 게 정체성에 맞다고 판단, 대신 옅은 틴트 + 배지로 일시정지 상태만 표시.
  • 기존 "러닝 종료" 클릭 핸들러(showRecord())는 로직 변경 없이 새 종료 버튼(btn_run_stop)에 그대로 재연결 — 종료 시 EndRunActivity로 이동하는 기존 동작은 동일.
  • 디자인 시안: https://claude.ai/code/artifact/5b40eebb-3e54-400f-b951-07e186cedf28 (기존 colors.xml/activity_run.xml 토큰만 사용, 신규 컬러 없음)

Test Plan

  • ./gradlew :app:compileDebugKotlin, :app:assembleDebug 로컬 빌드 성공 확인
  • 실기기/에뮬레이터 수동 QA 필요 — adb 환경이 없어 이 세션에서는 직접 실행/육안 확인을 하지 못했음. 일시정지→재개 시 타이머가 멈춘 지점부터 이어지는지, 버튼 전환이 의도대로 보이는지 직접 확인 필요.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added pause and resume controls during an active run.
    • Preserves elapsed time when paused and updates the run notification.
    • Added a dedicated stop-run action and paused-state indicator.
    • Updated controls with new icons, labels, and accessible text.

기존엔 러닝 종료 버튼 하나만 있어 중간에 멈출 수 없었음. 기존
"러닝 종료" 풀와이드 버튼을 원형 일시정지(주) 버튼으로 교체하고,
일시정지 시에만 종료(정지)/재개 버튼 두 개가 나타나도록 했다.
지도는 NRC처럼 별도 화면으로 바꾸지 않고 그대로 유지하되, 옅은
틴트와 "일시정지 중" 배지로 상태를 표시해 코스 경로를 계속 보여준다.

TimerService에 pauseTimer/resumeTimer를 추가했는데, 기존 time
값을 초기화하지 않는 startTimer/stopTimer 구조를 그대로 재사용해
일시정지 후 재개하면 멈췄던 지점부터 이어서 카운트된다.
@unam98 unam98 self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Run pause flow

Layer / File(s) Summary
Timer pause state
app/src/main/java/com/runnect/runnect/presentation/run/RunViewModel.kt, app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt
RunViewModel exposes isPaused. TimerService pauses scheduled updates, preserves elapsed time, and resumes from the preserved value.
Run screen controls
app/src/main/res/drawable/*, app/src/main/res/layout/activity_run.xml, app/src/main/res/values/strings.xml
The screen adds pause/resume and stop controls, paused-state styling, related icons, labels, and updated location-button constraints.
Activity control wiring
app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt
RunActivity connects pause/resume actions to TimerService, updates state and accessibility text, toggles paused controls, and moves completion handling to btnRunStop.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding pause and resume functionality to the running screen.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/run-pause-resume

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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Initialize timerData before stop can use it.

timerData is lateinit and is only assigned by timerReceiver, but the stop path reads it immediately after calling stopTimer(). If stop runs before the first timer broadcast, RunToEndRunData construction accesses an uninitialized property. Initialize timerData to zero time before enabling stop, or pass the current elapsed time from TimerService when 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

📥 Commits

Reviewing files that changed from the base of the PR and between d7cb241 and d2b97df.

📒 Files selected for processing (11)
  • app/src/main/java/com/runnect/runnect/presentation/run/RunActivity.kt
  • app/src/main/java/com/runnect/runnect/presentation/run/RunViewModel.kt
  • app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt
  • app/src/main/res/drawable/bg_paused_pill.xml
  • app/src/main/res/drawable/circle_ghost_button.xml
  • app/src/main/res/drawable/circle_m1_button.xml
  • app/src/main/res/drawable/ic_run_pause.xml
  • app/src/main/res/drawable/ic_run_resume.xml
  • app/src/main/res/drawable/ic_run_stop.xml
  • app/src/main/res/layout/activity_run.xml
  • app/src/main/res/values/strings.xml

getCurrentLocation()
showRecord()
backButton()
setUpPauseResume()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 || true

Repository: 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 || true

Repository: 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.kt

Repository: 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.

Comment on lines +137 to +146
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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 || true

Repository: 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});
JS

Repository: 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.

Suggested change
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.

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.

1 participant