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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ android {
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = kakaoNativeAppKey
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"${kakaoNativeAppKey}\"")
buildConfigField("String", "KAKAO_REST_API_KEY", "\"${kakaoRestApiKey}\"")
resValue("bool", "timer_sync_enabled", "true")

ndkVersion = "28.0.10027231"

Expand Down Expand Up @@ -93,6 +94,7 @@ android {
} else {
signingConfigs.getByName("debug")
}
resValue("bool", "timer_sync_enabled", "false")
resValue("string", "app_name", "운다방(E2E)")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.project200.undabang.di

import com.project200.common.utils.ChatRoomStateRepository
import com.project200.domain.manager.FcmTokenSyncScheduler
import com.project200.domain.manager.TimerSyncScheduler
import com.project200.undabang.fcm.ChatRoomStateRepositoryImpl
import com.project200.undabang.fcm.FcmTokenSyncSchedulerImpl
import com.project200.undabang.timer.TimerSyncSchedulerImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -20,4 +22,8 @@ abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindFcmTokenSyncScheduler(impl: FcmTokenSyncSchedulerImpl): FcmTokenSyncScheduler

@Binds
@Singleton
abstract fun bindTimerSyncScheduler(impl: TimerSyncSchedulerImpl): TimerSyncScheduler
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.project200.common.utils.NetworkMonitor
import com.project200.domain.manager.FcmTokenSyncScheduler
import com.project200.domain.manager.TimerSyncScheduler
import com.project200.domain.model.SessionExitReason
import com.project200.domain.model.UpdateCheckResult
import com.project200.domain.usecase.CheckForUpdateUseCase
Expand All @@ -30,6 +31,7 @@ class MainViewModel
constructor(
private val checkForUpdateUseCase: CheckForUpdateUseCase,
private val fcmTokenSyncScheduler: FcmTokenSyncScheduler,
private val timerSyncScheduler: TimerSyncScheduler,
private val networkMonitor: NetworkMonitor,
private val authManager: AuthManager,
private val authStateManager: AuthStateManager,
Expand Down Expand Up @@ -117,6 +119,7 @@ class MainViewModel
}
// 등록은 워커에 맡김. 재시도와 백오프, 프로세스 종료 후 재개를 WorkManager가 처리
fcmTokenSyncScheduler.schedule()
timerSyncScheduler.schedule()
_entryState.compareAndSet(EntryState.Loading, EntryState.Content)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.project200.undabang.timer

/** TimerSyncWorker의 재시도 횟수를 제한합니다 */
object TimerSyncPolicy {
const val MAX_ATTEMPT_COUNT = 10

fun shouldRetry(runAttemptCount: Int): Boolean = runAttemptCount + 1 < MAX_ATTEMPT_COUNT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.project200.undabang.timer

import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.project200.data.local.PreferenceManager
import com.project200.domain.manager.TimerSyncScheduler
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TimerSyncSchedulerImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
private val preferenceManager: PreferenceManager,
) : TimerSyncScheduler {
override fun schedule() {
// E2E는 로컬 타이머 목록 동작을 검증하므로 서버 동기화로 선택자가 바뀌지 않게 합니다
if (!isTimerSyncEnabled()) return
val memberId = preferenceManager.getMemberId() ?: return
val input = Data.Builder().putString(TimerSyncWorker.KEY_MEMBER_ID, memberId).build()
val request =
OneTimeWorkRequestBuilder<TimerSyncWorker>()
.setInputData(input)
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, BACKOFF_DELAY_SECONDS, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, request)
}

override fun cancel() {
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
}

private fun isTimerSyncEnabled(): Boolean {
val resourceId =
context.resources.getIdentifier(
TIMER_SYNC_ENABLED_RESOURCE,
"bool",
context.packageName,
)
return resourceId == 0 || context.resources.getBoolean(resourceId)
}

private companion object {
const val TIMER_SYNC_ENABLED_RESOURCE = "timer_sync_enabled"
const val WORK_NAME = TimerSyncWorker.WORK_NAME
const val BACKOFF_DELAY_SECONDS = 30L
}
}
45 changes: 45 additions & 0 deletions app/src/main/java/com/project200/undabang/timer/TimerSyncWorker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.project200.undabang.timer

import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.project200.domain.model.TimerSyncResult
import com.project200.domain.usecase.GetMemberIdUseCase
import com.project200.domain.usecase.SyncTimersUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber

@HiltWorker
class TimerSyncWorker
@AssistedInject
constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val getMemberIdUseCase: GetMemberIdUseCase,
private val syncTimersUseCase: SyncTimersUseCase,
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
val requestedMemberId = inputData.getString(KEY_MEMBER_ID) ?: return Result.success()
val currentMemberId = getMemberIdUseCase() ?: return Result.success()
if (currentMemberId != requestedMemberId) return Result.success()

return when (syncTimersUseCase(requestedMemberId)) {
TimerSyncResult.Success,
TimerSyncResult.Skipped,
-> Result.success()
TimerSyncResult.Retry -> {
Timber.tag(TAG).w("타이머 동기화 재시도: 시도 ${runAttemptCount + 1}회")
if (TimerSyncPolicy.shouldRetry(runAttemptCount)) Result.retry() else Result.failure()
}
TimerSyncResult.Failure -> Result.failure()
}
}

companion object {
const val WORK_NAME = "timer_sync"
const val KEY_MEMBER_ID = "member_id"
private const val TAG = "TimerSyncWorker"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.project200.undabang.main
import com.google.common.truth.Truth.assertThat
import com.project200.common.utils.NetworkMonitor
import com.project200.domain.manager.FcmTokenSyncScheduler
import com.project200.domain.manager.TimerSyncScheduler
import com.project200.domain.model.SessionExitReason
import com.project200.domain.model.UpdateCheckResult
import com.project200.domain.usecase.CheckForUpdateUseCase
Expand Down Expand Up @@ -45,6 +46,9 @@ class MainViewModelTest {
@MockK
private lateinit var mockFcmTokenSyncScheduler: FcmTokenSyncScheduler

@MockK
private lateinit var mockTimerSyncScheduler: TimerSyncScheduler

@MockK
private lateinit var mockNetworkMonitor: NetworkMonitor

Expand Down Expand Up @@ -73,8 +77,9 @@ class MainViewModelTest {
forceLogoutFlow = MutableSharedFlow(extraBufferCapacity = 1)
every { mockNetworkMonitor.networkState } returns networkStateFlow
every { mockNetworkMonitor.isCurrentlyConnected() } returns true
every { mockAuthManager.forceLogoutFlow } returns forceLogoutFlow
every { mockFcmTokenSyncScheduler.schedule() } just Runs
every { mockTimerSyncScheduler.schedule() } just Runs
every { mockAuthManager.forceLogoutFlow } returns forceLogoutFlow
coEvery { mockClearSessionUseCase(SessionExitReason.FORCED) } just Runs
}

Expand All @@ -87,6 +92,7 @@ class MainViewModelTest {
return MainViewModel(
checkForUpdateUseCase = mockCheckForUpdateUseCase,
fcmTokenSyncScheduler = mockFcmTokenSyncScheduler,
timerSyncScheduler = mockTimerSyncScheduler,
networkMonitor = mockNetworkMonitor,
authManager = mockAuthManager,
authStateManager = mockAuthStateManager,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.project200.undabang.timer

import com.google.common.truth.Truth.assertThat
import org.junit.Test

class TimerSyncPolicyTest {
@Test
fun `마지막 시도 전까지 재시도한다`() {
assertThat(TimerSyncPolicy.shouldRetry(TimerSyncPolicy.MAX_ATTEMPT_COUNT - 2)).isTrue()
assertThat(TimerSyncPolicy.shouldRetry(TimerSyncPolicy.MAX_ATTEMPT_COUNT - 1)).isFalse()
}
}
Loading
Loading