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
231 changes: 231 additions & 0 deletions core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package com.yapp.datastore

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
import java.time.Clock
import java.time.Instant
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class FortunePreferences @Inject constructor(
private val dataStore: DataStore<Preferences>,
private val clock: Clock,
) {
private object Keys {
val ID = longPreferencesKey("fortune_id")
val DATE = longPreferencesKey("fortune_date_epoch")
val IMAGE_ID = intPreferencesKey("fortune_image_id")
val SCORE = intPreferencesKey("fortune_score")
val SEEN = booleanPreferencesKey("fortune_seen")
val TOOLTIP_SHOWN = booleanPreferencesKey("fortune_tooltip_shown")

val CREATING = booleanPreferencesKey("fortune_creating")
val FAILED = booleanPreferencesKey("fortune_failed")

val ATTEMPT_ID = stringPreferencesKey("fortune_attempt_id")
val STARTED_AT = longPreferencesKey("fortune_started_at")
val EXPIRES_AT = longPreferencesKey("fortune_expires_at")

val FIRST_ALARM_DISMISSED_TODAY = booleanPreferencesKey("first_alarm_dismissed_today")
val FIRST_ALARM_DISMISSED_DATE_EPOCH = longPreferencesKey("first_alarm_dismissed_date_epoch")
}

private fun todayEpoch(): Long = LocalDate.now(clock).toEpochDay()
private fun nowMillis(): Long = Instant.now(clock).toEpochMilli()

val fortuneIdFlow: Flow<Long?> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.ID] }
.distinctUntilChanged()

val fortuneDateEpochFlow: Flow<Long?> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.DATE] }
.distinctUntilChanged()

val fortuneImageIdFlow: Flow<Int?> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.IMAGE_ID] }
.distinctUntilChanged()

val fortuneScoreFlow: Flow<Int?> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.SCORE] }
.distinctUntilChanged()

val hasUnseenFortuneFlow: Flow<Boolean> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { pref ->
val isToday = pref[Keys.DATE] == todayEpoch()
isToday && (pref[Keys.ID] != null) && (pref[Keys.SEEN] != true)
}
.distinctUntilChanged()

val shouldShowFortuneToolTipFlow: Flow<Boolean> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { pref ->
val hasTodayFortune = (pref[Keys.DATE] == todayEpoch()) && (pref[Keys.ID] != null)
val tooltipShown = pref[Keys.TOOLTIP_SHOWN] ?: false
hasTodayFortune && !tooltipShown
}
.distinctUntilChanged()

@OptIn(ExperimentalCoroutinesApi::class)
val isFortuneCreatingFlow: Flow<Boolean> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { pref ->
Triple(
pref[Keys.CREATING] ?: false,
pref[Keys.EXPIRES_AT] ?: 0L,
pref[Keys.ATTEMPT_ID],
)
}
.transformLatest { (creating, expiresAt, attemptId) ->
if (creating) {
val legacy = (expiresAt <= 0L) || attemptId.isNullOrEmpty()
val expired = (!legacy && nowMillis() > expiresAt)

if (legacy || expired) {
// 레거시(만료정보 없음) 또는 만료 → 실패로 교정
dataStore.edit { pref ->
pref[Keys.CREATING] = false
pref[Keys.FAILED] = true
}
emit(false)
return@transformLatest
}
}
emit(creating)
}
.distinctUntilChanged()

val isFortuneFailedFlow: Flow<Boolean> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.FAILED] ?: false }
.distinctUntilChanged()

val isFirstAlarmDismissedTodayFlow: Flow<Boolean> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { pref ->
val flag = pref[Keys.FIRST_ALARM_DISMISSED_TODAY] ?: false
val isToday = pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] == todayEpoch()
flag && isToday
}
.distinctUntilChanged()

suspend fun markFortuneCreating(
attemptId: String,
lease: Long,
) {
val now = nowMillis()
dataStore.edit { pref ->
pref[Keys.CREATING] = true
pref[Keys.FAILED] = false
pref[Keys.ATTEMPT_ID] = attemptId
pref[Keys.STARTED_AT] = now
pref[Keys.EXPIRES_AT] = now + lease
}
}

suspend fun markFortuneCreatedIfAttemptMatches(
attemptId: String,
fortuneId: Long,
) {
dataStore.edit { pref ->
val currentAttempt = pref[Keys.ATTEMPT_ID]
val isCreating = pref[Keys.CREATING] ?: false
val expiresAt = pref[Keys.EXPIRES_AT] ?: 0L

if (isCreating) {
val legacy = (expiresAt <= 0L) || currentAttempt.isNullOrEmpty()
val expired = (!legacy && nowMillis() > expiresAt)

if (legacy || expired) {
// 만료된 상태라면 성공 처리 거부
return@edit
}
}

if (isCreating && currentAttempt == attemptId) {
val today = todayEpoch()
val prevDate = pref[Keys.DATE]
val isNewForToday = (pref[Keys.ID] != fortuneId) || (prevDate != today)

pref[Keys.ID] = fortuneId
pref[Keys.DATE] = today
pref[Keys.CREATING] = false
pref[Keys.FAILED] = false
pref.remove(Keys.ATTEMPT_ID)
pref.remove(Keys.STARTED_AT)
pref.remove(Keys.EXPIRES_AT)

if (isNewForToday) {
pref[Keys.SEEN] = false
pref[Keys.TOOLTIP_SHOWN] = false
}
}
}
}

suspend fun markFortuneFailedIfAttemptMatches(attemptId: String) {
dataStore.edit { pref ->
if (pref[Keys.ATTEMPT_ID] == attemptId) {
pref[Keys.CREATING] = false
pref[Keys.FAILED] = true
pref.remove(Keys.ATTEMPT_ID)
pref.remove(Keys.STARTED_AT)
pref.remove(Keys.EXPIRES_AT)
}
}
}

suspend fun markFortuneSeen() {
dataStore.edit { it[Keys.SEEN] = true }
}

suspend fun markFortuneTooltipShown() {
dataStore.edit { it[Keys.TOOLTIP_SHOWN] = true }
}

suspend fun saveFortuneImageId(imageResId: Int) {
dataStore.edit { it[Keys.IMAGE_ID] = imageResId }
}

suspend fun saveFortuneScore(score: Int) {
dataStore.edit { it[Keys.SCORE] = score }
}

suspend fun markFirstAlarmDismissedToday() {
dataStore.edit { pref ->
pref[Keys.FIRST_ALARM_DISMISSED_TODAY] = true
pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] = todayEpoch()
}
}

suspend fun clearFortuneData() {
dataStore.edit { pref ->
pref.remove(Keys.ID)
pref.remove(Keys.DATE)
pref.remove(Keys.IMAGE_ID)
pref.remove(Keys.SCORE)
pref.remove(Keys.SEEN)
pref.remove(Keys.TOOLTIP_SHOWN)
pref.remove(Keys.CREATING)
pref.remove(Keys.FAILED)
}
}
}
Loading