Skip to content
Merged
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
9 changes: 3 additions & 6 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,10 @@ jobs:
groups: testers
file: app/build/outputs/apk/prod/release/app-prod-release.apk
releaseNotes: |
RunCombi Android v${{ steps.app_version.outputs.version_name }}
🚀 RunCombi Android v${{ steps.app_version.outputs.version_name }} 배포 완료!

PR: ${{ github.event.pull_request.title }}
Author: @${{ github.event.pull_request.user.login }}
Branch: ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}

APK: Prod Release Version
📱 **앱 버전**: v${{ steps.app_version.outputs.version_name }} (${{ steps.app_version.outputs.version_code }})
notifyTesters: true

- name: If Success, Send notification on Slack
if: ${{success()}}
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ android {
applicationId = "com.combo.runcombi"
minSdk = 26
targetSdk = 35
versionCode = 107
versionName = "1.0.7"
versionCode = 108
versionName = "1.0.8"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
6 changes: 6 additions & 0 deletions feature/walk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ dependencies {
implementation(libs.lottie.compose)

implementation(libs.androidx.graphics.shapes)
implementation(libs.gson)

// Core 모듈 의존성
implementation(project(":core:data:common"))
implementation(project(":core:domain:walk"))
implementation(project(":core:data:walk"))
}
14 changes: 14 additions & 0 deletions feature/walk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application>
<service
android:name="com.combo.runcombi.walk.service.WalkTrackingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.combo.runcombi.walk.navigation

import WalkMainScreen
import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
Expand All @@ -10,7 +11,6 @@ import androidx.navigation.compose.navigation
import com.combo.runcombi.core.navigation.model.MainTabDataModel
import com.combo.runcombi.core.navigation.model.RouteModel
import com.combo.runcombi.walk.screen.WalkCountdownScreen
import com.combo.runcombi.walk.screen.WalkMainScreen
import com.combo.runcombi.walk.screen.WalkReadyScreen
import com.combo.runcombi.walk.screen.WalkResultScreen
import com.combo.runcombi.walk.screen.WalkTrackingScreen
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.combo.runcombi.walk.screen

import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.provider.Settings
import android.widget.Toast
import androidx.compose.foundation.background
Expand Down Expand Up @@ -91,6 +90,8 @@ fun WalkMainScreen(
val cameraPositionState = rememberCameraPositionState()
val uiState by walkMainViewModel.uiState.collectAsStateWithLifecycle()
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
val notificationPermissionState =
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
var showPermissionSettingSheet by remember { mutableStateOf(false) }
val analyticsHelper = walkMainViewModel.analyticsHelper

Expand All @@ -100,9 +101,14 @@ fun WalkMainScreen(
if (!locationPermissionState.status.isGranted) {
locationPermissionState.launchPermissionRequest()
}
// 화면 진입 시 사용자 정보 갱신

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
notificationPermissionState.launchPermissionRequest()
}

walkMainViewModel.fetchUserAndPets()
}

LaunchedEffect(locationPermissionState.status.isGranted) {
if (locationPermissionState.status.isGranted) {
val myLocation = LocationUtil.getCurrentLocation(context)
Expand Down Expand Up @@ -163,13 +169,20 @@ fun WalkMainScreen(
isLocationPermissionGranted = locationPermissionState.status.isGranted,
onPetClick = { walkMainViewModel.togglePetSelect(it) },
onStartWalk = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationPermissionState.status.isGranted) {
notificationPermissionState.launchPermissionRequest()
}

if (!locationPermissionState.status.isGranted) {
if (locationPermissionState.status.shouldShowRationale) {
locationPermissionState.launchPermissionRequest()
} else {
walkMainViewModel.onStartWalkClicked(false)
}
} else if (walkMainViewModel.checkWithInitWalkData()) {
return@WalkMainContent
}

if (walkMainViewModel.checkWithInitWalkData()) {
onStartWalk()
} else {
Toast.makeText(context, "함께 운동할 콤비를 선택해주세요.", Toast.LENGTH_SHORT).show()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.combo.runcombi.walk.screen

import android.annotation.SuppressLint
import android.content.res.Configuration
import android.os.Looper
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand All @@ -22,7 +21,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -72,12 +70,6 @@ import com.combo.runcombi.walk.model.WalkUiState
import com.combo.runcombi.walk.model.getBottomSheetContent
import com.combo.runcombi.walk.viewmodel.WalkMainViewModel
import com.combo.runcombi.walk.viewmodel.WalkTrackingViewModel
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest

@SuppressLint("MissingPermission")
Expand All @@ -92,35 +84,22 @@ fun WalkTrackingScreen(
val analyticsHelper = walkRecordViewModel.analyticsHelper
val uiState by walkRecordViewModel.uiState.collectAsStateWithLifecycle()
val isPaused = uiState.isPaused
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }

val isInitialized = rememberSaveable { mutableStateOf(false) }

val showSheet = remember { mutableStateOf(BottomSheetType.NONE) }

val locationCallback = remember {
object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { location ->
walkRecordViewModel.addPathPointFromService(
location.latitude, location.longitude, location.accuracy, location.time
)
}
}
}
}

LaunchedEffect(isInitialized.value) {
if (!isInitialized.value) {
analyticsHelper.logScreenView("WalkTrackingScreen")

walkMainViewModel.startRun()

val member = walkMainViewModel.walkData.value.member
val exerciseType = walkMainViewModel.walkData.value.exerciseType
val selectedPetList = walkMainViewModel.walkData.value.petList

if (member != null) {
walkRecordViewModel.initWalkData(exerciseType, member, selectedPetList)

walkMainViewModel.startRun()
}
isInitialized.value = true
}
Expand All @@ -132,27 +111,6 @@ fun WalkTrackingScreen(
}
}

DisposableEffect(isPaused) {
if (!isPaused) {
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000)
.setMinUpdateIntervalMillis(1000).build()

fusedLocationClient.requestLocationUpdates(
request, locationCallback, Looper.getMainLooper()
)
}
onDispose {
fusedLocationClient.removeLocationUpdates(locationCallback)
}
}

LaunchedEffect(isPaused) {
while (!isPaused) {
walkRecordViewModel.updateTime(uiState.time + 1)
delay(1000)
}
}

WalkTrackingContent(
uiState = uiState,
onPauseToggle = walkRecordViewModel::togglePause,
Expand All @@ -170,7 +128,6 @@ fun WalkTrackingScreen(
onAccept = {
when (showSheet.value) {
BottomSheetType.FINISH -> {
// 산책 완료 이벤트 로깅
val duration = FormatUtils.formatTime(uiState.time)
val distance = String.format("%.2f", uiState.distance / 1000.0)
analyticsHelper.logWalkCompleted(duration, "${distance}km")
Expand All @@ -182,10 +139,15 @@ fun WalkTrackingScreen(
member = uiState.walkMemberUiModel,
petList = uiState.walkPetUIModelList ?: emptyList()
)

walkRecordViewModel.stopTracking()
onFinish()
}

BottomSheetType.CANCEL -> onBack()
BottomSheetType.CANCEL -> {
walkRecordViewModel.stopTracking()
onBack()
}
else -> Unit
}
showSheet.value = BottomSheetType.NONE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.combo.runcombi.walk.service

import android.util.Log
import com.combo.runcombi.walk.model.LocationPoint
import com.combo.runcombi.walk.model.WalkMemberUiModel
import com.combo.runcombi.walk.model.WalkPetUIModel
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class WalkTrackingDataManager @Inject constructor() {
companion object {
private const val TAG = "WalkTrackingDataManager"
}

private val _trackingData = MutableStateFlow(TrackingData())
val trackingData: StateFlow<TrackingData> = _trackingData.asStateFlow()

fun updateLocationData(
pathPoints: List<LatLng>,
distance: Double,
time: Int
) {
Log.d(TAG, "updateLocationData: 경로점=${pathPoints.size}, 거리=${distance}m, 시간=${time}초")
_trackingData.update { it.copy(
pathPoints = pathPoints,
distance = distance,
time = time
) }
}

fun updateMemberData(member: WalkMemberUiModel?) {
Log.d(TAG, "updateMemberData: 멤버 업데이트 - $member")
_trackingData.update { it.copy(member = member) }
}

fun updatePetListData(petList: List<WalkPetUIModel>?) {
Log.d(TAG, "updatePetListData: 반려동물 리스트 업데이트 - $petList")
_trackingData.update { it.copy(petList = petList) }
}

fun updateExerciseType(exerciseType: String) {
Log.d(TAG, "updateExerciseType: 운동 타입 업데이트 - $exerciseType")
_trackingData.update { it.copy(exerciseType = exerciseType) }
}

fun updateInitialData(
exerciseType: String,
member: WalkMemberUiModel,
petList: List<WalkPetUIModel>
) {
Log.d(TAG, "updateInitialData: 초기 데이터 설정 - 타입:$exerciseType, 멤버:${member}, 반려동물:${petList}")
_trackingData.update { it.copy(
exerciseType = exerciseType,
member = member,
petList = petList,
time = 0,
distance = 0.0,
pathPoints = emptyList(),
isPaused = false,
isTracking = true
) }
}

fun updatePauseState(isPaused: Boolean) {
Log.d(TAG, "updatePauseState: 일시정지 상태 업데이트 - $isPaused")
_trackingData.update { it.copy(isPaused = isPaused) }
}

fun updateTrackingState(isTracking: Boolean) {
Log.d(TAG, "updateTrackingState: 추적 상태 업데이트 - $isTracking")
_trackingData.update { it.copy(isTracking = isTracking) }
}

fun resetData() {
Log.d(TAG, "resetData: 추적 데이터 초기화")
_trackingData.value = TrackingData()
}

data class TrackingData(
val exerciseType: String = "",
val time: Int = 0,
val distance: Double = 0.0,
val pathPoints: List<LatLng> = emptyList(),
val member: WalkMemberUiModel? = null,
val petList: List<WalkPetUIModel>? = null,
val isPaused: Boolean = false,
val isTracking: Boolean = false
)
}
Loading
Loading