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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ trim_trailing_whitespace = true
[*.{kt,kts}]
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_annotation = disabled

[*Test.kt]
ktlint_standard_function-naming = disabled
4 changes: 4 additions & 0 deletions feature/auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ dependencies {

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)

testImplementation(libs.junit)
testImplementation(libs.kotlin.test)
testImplementation(libs.kotlinx.coroutines.test)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.util.Log
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.viewModelScope
import com.sseotdabwa.buyornot.core.ui.base.BaseViewModel
import com.sseotdabwa.buyornot.domain.model.AppUpdateInfo
import com.sseotdabwa.buyornot.domain.model.UpdateStrategy
import com.sseotdabwa.buyornot.domain.model.UserType
import com.sseotdabwa.buyornot.domain.repository.AppPreferencesRepository
Expand All @@ -20,9 +21,36 @@ import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException

private const val SPLASH_TIMEOUT_MILLIS = 2300L
private const val SOFT_UPDATE_INTERVAL_MILLIS = 24 * 60 * 60 * 1000L
internal const val SOFT_UPDATE_INTERVAL_MILLIS = 24 * 60 * 60 * 1000L
private const val TAG = "SplashUpdate"

internal fun resolveUpdateDialogType(
currentVersion: Int,
updateInfo: AppUpdateInfo?,
lastSoftUpdateShownTime: Long,
now: Long,
): UpdateDialogType {
if (updateInfo == null) return UpdateDialogType.None

return when {
currentVersion < updateInfo.minimumVersion -> UpdateDialogType.Force
// currentVersion >= latestVersion이면 이미 최신 버전이므로 FORCE 팝업 표시 안 함
updateInfo.updateStrategy == UpdateStrategy.FORCE &&
currentVersion < updateInfo.latestVersion -> UpdateDialogType.Force
updateInfo.updateStrategy == UpdateStrategy.SOFT &&
currentVersion < updateInfo.latestVersion -> {
// lastSoftUpdateShownTime이 미래 값이면 시계 역행으로 판단, 표시된 적 없는 것으로 처리
val effectiveLastShown = if (lastSoftUpdateShownTime > now) 0L else lastSoftUpdateShownTime
if (now - effectiveLastShown >= SOFT_UPDATE_INTERVAL_MILLIS) {
UpdateDialogType.Soft
} else {
UpdateDialogType.None
}
}
else -> UpdateDialogType.None
}
}

/**
* 스플래시 화면을 위한 ViewModel
*
Expand Down Expand Up @@ -93,30 +121,21 @@ class SplashViewModel @Inject constructor(

private suspend fun determineDialogType(
currentVersion: Int,
updateInfo: com.sseotdabwa.buyornot.domain.model.AppUpdateInfo?,
updateInfo: AppUpdateInfo?,
): UpdateDialogType {
if (updateInfo == null) return UpdateDialogType.None

return when {
currentVersion < updateInfo.minimumVersion -> UpdateDialogType.Force
updateInfo.updateStrategy == UpdateStrategy.FORCE -> UpdateDialogType.Force
updateInfo.updateStrategy == UpdateStrategy.SOFT &&
currentVersion < updateInfo.latestVersion -> {
val lastShown = appPreferencesRepository.lastSoftUpdateShownTime.first()
if (System.currentTimeMillis() - lastShown >= SOFT_UPDATE_INTERVAL_MILLIS) {
UpdateDialogType.Soft
} else {
UpdateDialogType.None
}
}
else -> UpdateDialogType.None
}
val now = System.currentTimeMillis()
val lastShown = appPreferencesRepository.lastSoftUpdateShownTime.first()
return resolveUpdateDialogType(currentVersion, updateInfo, lastShown, now)
}

private fun dismissSoftUpdate() {
viewModelScope.launch {
try {
appPreferencesRepository.updateLastSoftUpdateShownTime(System.currentTimeMillis())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Failed to save soft update shown time", e)
} finally {
updateState { it.copy(updateDialogType = UpdateDialogType.None) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package com.sseotdabwa.buyornot.feature.auth.ui

import com.sseotdabwa.buyornot.domain.model.AppUpdateInfo
import com.sseotdabwa.buyornot.domain.model.UpdateStrategy
import org.junit.Test
import kotlin.test.assertEquals

class SplashUpdateLogicTest {
private val now = 1_000_000_000L

@Test
fun updateInfo가_null이면_None을_반환한다() {
val result =
resolveUpdateDialogType(
currentVersion = 10,
updateInfo = null,
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(UpdateDialogType.None, result)
}

@Test
fun currentVersion이_minimumVersion_미만이면_Force를_반환한다() {
val result =
resolveUpdateDialogType(
currentVersion = 5,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 6,
updateStrategy = UpdateStrategy.NONE,
),
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(UpdateDialogType.Force, result)
}

@Test
fun FORCE_전략이고_currentVersion이_latestVersion_미만이면_Force를_반환한다() {
val result =
resolveUpdateDialogType(
currentVersion = 8,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.FORCE,
),
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(UpdateDialogType.Force, result)
}

@Test
fun FORCE_전략이지만_currentVersion이_latestVersion_이상이면_None을_반환한다() {
val result =
resolveUpdateDialogType(
currentVersion = 10,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.FORCE,
),
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(UpdateDialogType.None, result)
}

@Test
fun FORCE_전략이고_minimumVersion_이상_latestVersion_미만이면_Force를_반환한다() {
val result =
resolveUpdateDialogType(
currentVersion = 7,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.FORCE,
),
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(UpdateDialogType.Force, result)
}

@Test
fun SOFT_전략이고_24시간_이상_지났으면_Soft를_반환한다() {
val lastShown = now - SOFT_UPDATE_INTERVAL_MILLIS
val result =
resolveUpdateDialogType(
currentVersion = 8,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.SOFT,
),
lastSoftUpdateShownTime = lastShown,
now = now,
)
assertEquals(UpdateDialogType.Soft, result)
}

@Test
fun SOFT_전략이지만_24시간_미만이면_None을_반환한다() {
val lastShown = now - SOFT_UPDATE_INTERVAL_MILLIS + 1
val result =
resolveUpdateDialogType(
currentVersion = 8,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.SOFT,
),
lastSoftUpdateShownTime = lastShown,
now = now,
)
assertEquals(UpdateDialogType.None, result)
}

@Test
fun SOFT_전략이지만_currentVersion이_latestVersion_이상이면_None을_반환한다() {
val result =
resolveUpdateDialogType(
currentVersion = 10,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.SOFT,
),
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(UpdateDialogType.None, result)
}

@Test
fun 시계가_역행했을때_lastShown이_무효화되어_Soft를_반환한다() {
// lastSoftUpdateShownTime이 now보다 미래인 경우 (시계 역행) → effectiveLastShown = 0 → 항상 Soft
val lastShownInFuture = now + 1_000L
val result =
resolveUpdateDialogType(
currentVersion = 8,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.SOFT,
),
lastSoftUpdateShownTime = lastShownInFuture,
now = now,
)
assertEquals(UpdateDialogType.Soft, result)
}

@Test
fun NONE_전략이면_None을_반환한다() {
val result =
resolveUpdateDialogType(
currentVersion = 8,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.NONE,
),
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(UpdateDialogType.None, result)
}

@Test
fun minimumVersion_미달이면_전략_무관하게_Force를_반환한다() {
for (strategy in UpdateStrategy.entries) {
val result =
resolveUpdateDialogType(
currentVersion = 3,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = strategy,
),
lastSoftUpdateShownTime = 0L,
now = now,
)
assertEquals(
UpdateDialogType.Force,
result,
"strategy=$strategy 일 때 minimumVersion 미달은 Force여야 함",
)
}
}

@Test
fun 최초_설치시_소프트_업데이트를_표시한다() {
// DataStore 기본값(0L)일 때 now - 0 >= SOFT_UPDATE_INTERVAL_MILLIS 이므로 Soft 반환
val result =
resolveUpdateDialogType(
currentVersion = 8,
updateInfo =
AppUpdateInfo(
latestVersion = 10,
minimumVersion = 5,
updateStrategy = UpdateStrategy.SOFT,
),
lastSoftUpdateShownTime = 0L,
now = SOFT_UPDATE_INTERVAL_MILLIS + 1L,
)
assertEquals(UpdateDialogType.Soft, result)
}
}
Loading