Skip to content

Commit 116cffe

Browse files
authored
Merge pull request #3 from Central-MakeUs/develop
포그라운드 서비스 추가
2 parents 50cb742 + 25d3617 commit 116cffe

12 files changed

Lines changed: 683 additions & 136 deletions

File tree

.github/workflows/android.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,10 @@ jobs:
218218
groups: testers
219219
file: app/build/outputs/apk/prod/release/app-prod-release.apk
220220
releaseNotes: |
221-
RunCombi Android v${{ steps.app_version.outputs.version_name }}
221+
🚀 RunCombi Android v${{ steps.app_version.outputs.version_name }} 배포 완료!
222222
223-
PR: ${{ github.event.pull_request.title }}
224-
Author: @${{ github.event.pull_request.user.login }}
225-
Branch: ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}
226-
227-
APK: Prod Release Version
223+
📱 **앱 버전**: v${{ steps.app_version.outputs.version_name }} (${{ steps.app_version.outputs.version_code }})
224+
notifyTesters: true
228225

229226
- name: If Success, Send notification on Slack
230227
if: ${{success()}}

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ android {
1414
applicationId = "com.combo.runcombi"
1515
minSdk = 26
1616
targetSdk = 35
17-
versionCode = 107
18-
versionName = "1.0.7"
17+
versionCode = 108
18+
versionName = "1.0.8"
1919

2020
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2121

feature/walk/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,10 @@ dependencies {
2323
implementation(libs.lottie.compose)
2424

2525
implementation(libs.androidx.graphics.shapes)
26+
implementation(libs.gson)
27+
28+
// Core 모듈 의존성
29+
implementation(project(":core:data:common"))
30+
implementation(project(":core:domain:walk"))
31+
implementation(project(":core:data:walk"))
2632
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33

4+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
5+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
6+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
7+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
8+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
9+
10+
<application>
11+
<service
12+
android:name="com.combo.runcombi.walk.service.WalkTrackingService"
13+
android:enabled="true"
14+
android:exported="false"
15+
android:foregroundServiceType="location" />
16+
</application>
17+
418
</manifest>

feature/walk/src/main/java/com/combo/runcombi/walk/navigation/WalkNavigation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.combo.runcombi.walk.navigation
22

3+
import WalkMainScreen
34
import androidx.compose.runtime.remember
45
import androidx.hilt.navigation.compose.hiltViewModel
56
import androidx.navigation.NavController
@@ -10,7 +11,6 @@ import androidx.navigation.compose.navigation
1011
import com.combo.runcombi.core.navigation.model.MainTabDataModel
1112
import com.combo.runcombi.core.navigation.model.RouteModel
1213
import com.combo.runcombi.walk.screen.WalkCountdownScreen
13-
import com.combo.runcombi.walk.screen.WalkMainScreen
1414
import com.combo.runcombi.walk.screen.WalkReadyScreen
1515
import com.combo.runcombi.walk.screen.WalkResultScreen
1616
import com.combo.runcombi.walk.screen.WalkTrackingScreen

feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkMainScreen.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
package com.combo.runcombi.walk.screen
2-
31
import android.Manifest
42
import android.annotation.SuppressLint
53
import android.content.Intent
64
import android.content.res.Configuration
5+
import android.os.Build
76
import android.provider.Settings
87
import android.widget.Toast
98
import androidx.compose.foundation.background
@@ -91,6 +90,8 @@ fun WalkMainScreen(
9190
val cameraPositionState = rememberCameraPositionState()
9291
val uiState by walkMainViewModel.uiState.collectAsStateWithLifecycle()
9392
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
93+
val notificationPermissionState =
94+
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
9495
var showPermissionSettingSheet by remember { mutableStateOf(false) }
9596
val analyticsHelper = walkMainViewModel.analyticsHelper
9697

@@ -100,9 +101,14 @@ fun WalkMainScreen(
100101
if (!locationPermissionState.status.isGranted) {
101102
locationPermissionState.launchPermissionRequest()
102103
}
103-
// 화면 진입 시 사용자 정보 갱신
104+
105+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
106+
notificationPermissionState.launchPermissionRequest()
107+
}
108+
104109
walkMainViewModel.fetchUserAndPets()
105110
}
111+
106112
LaunchedEffect(locationPermissionState.status.isGranted) {
107113
if (locationPermissionState.status.isGranted) {
108114
val myLocation = LocationUtil.getCurrentLocation(context)
@@ -163,13 +169,20 @@ fun WalkMainScreen(
163169
isLocationPermissionGranted = locationPermissionState.status.isGranted,
164170
onPetClick = { walkMainViewModel.togglePetSelect(it) },
165171
onStartWalk = {
172+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationPermissionState.status.isGranted) {
173+
notificationPermissionState.launchPermissionRequest()
174+
}
175+
166176
if (!locationPermissionState.status.isGranted) {
167177
if (locationPermissionState.status.shouldShowRationale) {
168178
locationPermissionState.launchPermissionRequest()
169179
} else {
170180
walkMainViewModel.onStartWalkClicked(false)
171181
}
172-
} else if (walkMainViewModel.checkWithInitWalkData()) {
182+
return@WalkMainContent
183+
}
184+
185+
if (walkMainViewModel.checkWithInitWalkData()) {
173186
onStartWalk()
174187
} else {
175188
Toast.makeText(context, "함께 운동할 콤비를 선택해주세요.", Toast.LENGTH_SHORT).show()

feature/walk/src/main/java/com/combo/runcombi/walk/screen/WalkTrackingScreen.kt

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.combo.runcombi.walk.screen
22

33
import android.annotation.SuppressLint
44
import android.content.res.Configuration
5-
import android.os.Looper
65
import androidx.compose.foundation.ExperimentalFoundationApi
76
import androidx.compose.foundation.background
87
import androidx.compose.foundation.clickable
@@ -22,7 +21,6 @@ import androidx.compose.foundation.layout.width
2221
import androidx.compose.foundation.shape.RoundedCornerShape
2322
import androidx.compose.material3.Text
2423
import androidx.compose.runtime.Composable
25-
import androidx.compose.runtime.DisposableEffect
2624
import androidx.compose.runtime.LaunchedEffect
2725
import androidx.compose.runtime.getValue
2826
import androidx.compose.runtime.mutableStateOf
@@ -72,12 +70,6 @@ import com.combo.runcombi.walk.model.WalkUiState
7270
import com.combo.runcombi.walk.model.getBottomSheetContent
7371
import com.combo.runcombi.walk.viewmodel.WalkMainViewModel
7472
import com.combo.runcombi.walk.viewmodel.WalkTrackingViewModel
75-
import com.google.android.gms.location.LocationCallback
76-
import com.google.android.gms.location.LocationRequest
77-
import com.google.android.gms.location.LocationResult
78-
import com.google.android.gms.location.LocationServices
79-
import com.google.android.gms.location.Priority
80-
import kotlinx.coroutines.delay
8173
import kotlinx.coroutines.flow.collectLatest
8274

8375
@SuppressLint("MissingPermission")
@@ -92,35 +84,22 @@ fun WalkTrackingScreen(
9284
val analyticsHelper = walkRecordViewModel.analyticsHelper
9385
val uiState by walkRecordViewModel.uiState.collectAsStateWithLifecycle()
9486
val isPaused = uiState.isPaused
95-
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
9687

9788
val isInitialized = rememberSaveable { mutableStateOf(false) }
98-
9989
val showSheet = remember { mutableStateOf(BottomSheetType.NONE) }
10090

101-
val locationCallback = remember {
102-
object : LocationCallback() {
103-
override fun onLocationResult(result: LocationResult) {
104-
result.lastLocation?.let { location ->
105-
walkRecordViewModel.addPathPointFromService(
106-
location.latitude, location.longitude, location.accuracy, location.time
107-
)
108-
}
109-
}
110-
}
111-
}
112-
11391
LaunchedEffect(isInitialized.value) {
11492
if (!isInitialized.value) {
11593
analyticsHelper.logScreenView("WalkTrackingScreen")
11694

117-
walkMainViewModel.startRun()
118-
11995
val member = walkMainViewModel.walkData.value.member
12096
val exerciseType = walkMainViewModel.walkData.value.exerciseType
12197
val selectedPetList = walkMainViewModel.walkData.value.petList
98+
12299
if (member != null) {
123100
walkRecordViewModel.initWalkData(exerciseType, member, selectedPetList)
101+
102+
walkMainViewModel.startRun()
124103
}
125104
isInitialized.value = true
126105
}
@@ -132,27 +111,6 @@ fun WalkTrackingScreen(
132111
}
133112
}
134113

135-
DisposableEffect(isPaused) {
136-
if (!isPaused) {
137-
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000)
138-
.setMinUpdateIntervalMillis(1000).build()
139-
140-
fusedLocationClient.requestLocationUpdates(
141-
request, locationCallback, Looper.getMainLooper()
142-
)
143-
}
144-
onDispose {
145-
fusedLocationClient.removeLocationUpdates(locationCallback)
146-
}
147-
}
148-
149-
LaunchedEffect(isPaused) {
150-
while (!isPaused) {
151-
walkRecordViewModel.updateTime(uiState.time + 1)
152-
delay(1000)
153-
}
154-
}
155-
156114
WalkTrackingContent(
157115
uiState = uiState,
158116
onPauseToggle = walkRecordViewModel::togglePause,
@@ -170,7 +128,6 @@ fun WalkTrackingScreen(
170128
onAccept = {
171129
when (showSheet.value) {
172130
BottomSheetType.FINISH -> {
173-
// 산책 완료 이벤트 로깅
174131
val duration = FormatUtils.formatTime(uiState.time)
175132
val distance = String.format("%.2f", uiState.distance / 1000.0)
176133
analyticsHelper.logWalkCompleted(duration, "${distance}km")
@@ -182,10 +139,15 @@ fun WalkTrackingScreen(
182139
member = uiState.walkMemberUiModel,
183140
petList = uiState.walkPetUIModelList ?: emptyList()
184141
)
142+
143+
walkRecordViewModel.stopTracking()
185144
onFinish()
186145
}
187146

188-
BottomSheetType.CANCEL -> onBack()
147+
BottomSheetType.CANCEL -> {
148+
walkRecordViewModel.stopTracking()
149+
onBack()
150+
}
189151
else -> Unit
190152
}
191153
showSheet.value = BottomSheetType.NONE
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.combo.runcombi.walk.service
2+
3+
import android.util.Log
4+
import com.combo.runcombi.walk.model.LocationPoint
5+
import com.combo.runcombi.walk.model.WalkMemberUiModel
6+
import com.combo.runcombi.walk.model.WalkPetUIModel
7+
import com.google.android.gms.maps.model.LatLng
8+
import kotlinx.coroutines.flow.MutableStateFlow
9+
import kotlinx.coroutines.flow.StateFlow
10+
import kotlinx.coroutines.flow.asStateFlow
11+
import kotlinx.coroutines.flow.update
12+
import javax.inject.Inject
13+
import javax.inject.Singleton
14+
15+
@Singleton
16+
class WalkTrackingDataManager @Inject constructor() {
17+
companion object {
18+
private const val TAG = "WalkTrackingDataManager"
19+
}
20+
21+
private val _trackingData = MutableStateFlow(TrackingData())
22+
val trackingData: StateFlow<TrackingData> = _trackingData.asStateFlow()
23+
24+
fun updateLocationData(
25+
pathPoints: List<LatLng>,
26+
distance: Double,
27+
time: Int
28+
) {
29+
Log.d(TAG, "updateLocationData: 경로점=${pathPoints.size}, 거리=${distance}m, 시간=${time}")
30+
_trackingData.update { it.copy(
31+
pathPoints = pathPoints,
32+
distance = distance,
33+
time = time
34+
) }
35+
}
36+
37+
fun updateMemberData(member: WalkMemberUiModel?) {
38+
Log.d(TAG, "updateMemberData: 멤버 업데이트 - $member")
39+
_trackingData.update { it.copy(member = member) }
40+
}
41+
42+
fun updatePetListData(petList: List<WalkPetUIModel>?) {
43+
Log.d(TAG, "updatePetListData: 반려동물 리스트 업데이트 - $petList")
44+
_trackingData.update { it.copy(petList = petList) }
45+
}
46+
47+
fun updateExerciseType(exerciseType: String) {
48+
Log.d(TAG, "updateExerciseType: 운동 타입 업데이트 - $exerciseType")
49+
_trackingData.update { it.copy(exerciseType = exerciseType) }
50+
}
51+
52+
fun updateInitialData(
53+
exerciseType: String,
54+
member: WalkMemberUiModel,
55+
petList: List<WalkPetUIModel>
56+
) {
57+
Log.d(TAG, "updateInitialData: 초기 데이터 설정 - 타입:$exerciseType, 멤버:${member}, 반려동물:${petList}")
58+
_trackingData.update { it.copy(
59+
exerciseType = exerciseType,
60+
member = member,
61+
petList = petList,
62+
time = 0,
63+
distance = 0.0,
64+
pathPoints = emptyList(),
65+
isPaused = false,
66+
isTracking = true
67+
) }
68+
}
69+
70+
fun updatePauseState(isPaused: Boolean) {
71+
Log.d(TAG, "updatePauseState: 일시정지 상태 업데이트 - $isPaused")
72+
_trackingData.update { it.copy(isPaused = isPaused) }
73+
}
74+
75+
fun updateTrackingState(isTracking: Boolean) {
76+
Log.d(TAG, "updateTrackingState: 추적 상태 업데이트 - $isTracking")
77+
_trackingData.update { it.copy(isTracking = isTracking) }
78+
}
79+
80+
fun resetData() {
81+
Log.d(TAG, "resetData: 추적 데이터 초기화")
82+
_trackingData.value = TrackingData()
83+
}
84+
85+
data class TrackingData(
86+
val exerciseType: String = "",
87+
val time: Int = 0,
88+
val distance: Double = 0.0,
89+
val pathPoints: List<LatLng> = emptyList(),
90+
val member: WalkMemberUiModel? = null,
91+
val petList: List<WalkPetUIModel>? = null,
92+
val isPaused: Boolean = false,
93+
val isTracking: Boolean = false
94+
)
95+
}

0 commit comments

Comments
 (0)