Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
212 changes: 212 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,212 @@
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]
if (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

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
}
}
}

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)
}
}
}
137 changes: 0 additions & 137 deletions core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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.flow.Flow
Expand All @@ -25,18 +24,6 @@ class UserPreferences @Inject constructor(
val USER_NAME = stringPreferencesKey("user_name")
val ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed")

val FORTUNE_ID = longPreferencesKey("fortune_id")
val FORTUNE_DATE_EPOCH = longPreferencesKey("fortune_date_epoch")
val FORTUNE_IMAGE_ID = intPreferencesKey("fortune_image_id")
val FORTUNE_SCORE = intPreferencesKey("fortune_score")
val FORTUNE_SEEN = booleanPreferencesKey("fortune_seen")
val FORTUNE_TOOLTIP_SHOWN = booleanPreferencesKey("fortune_tooltip_shown")
val FORTUNE_CREATING = booleanPreferencesKey("fortune_creating")
val FORTUNE_FAILED = booleanPreferencesKey("fortune_failed")

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

val UPDATE_NOTICE_DONT_SHOW_VERSION = stringPreferencesKey("update_notice_dont_show_version")
val UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH = longPreferencesKey("update_notice_last_shown_date_epoch")
}
Expand All @@ -58,62 +45,6 @@ class UserPreferences @Inject constructor(
.map { it[Keys.ONBOARDING_COMPLETED] ?: false }
.distinctUntilChanged()

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

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

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

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

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

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

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

val isFortuneFailedFlow: Flow<Boolean> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.FORTUNE_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()

val updateNoticeDontShowVersionFlow: Flow<String?> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] }
Expand All @@ -136,61 +67,6 @@ class UserPreferences @Inject constructor(
dataStore.edit { it[Keys.ONBOARDING_COMPLETED] = true }
}

suspend fun markFortuneCreating() {
dataStore.edit { pref ->
pref[Keys.FORTUNE_CREATING] = true
pref[Keys.FORTUNE_FAILED] = false
}
}

suspend fun markFortuneCreated(fortuneId: Long) {
dataStore.edit { pref ->
val today = todayEpoch()
val prevDate = pref[Keys.FORTUNE_DATE_EPOCH]
val isNewForToday = (pref[Keys.FORTUNE_ID] != fortuneId) || (prevDate != today)

pref[Keys.FORTUNE_ID] = fortuneId
pref[Keys.FORTUNE_DATE_EPOCH] = today
pref[Keys.FORTUNE_CREATING] = false
pref[Keys.FORTUNE_FAILED] = false

if (isNewForToday) {
pref[Keys.FORTUNE_SEEN] = false
pref[Keys.FORTUNE_TOOLTIP_SHOWN] = false
}
}
}

suspend fun markFortuneFailed() {
dataStore.edit { pref ->
pref[Keys.FORTUNE_CREATING] = false
pref[Keys.FORTUNE_FAILED] = true
}
}

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

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

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

suspend fun saveFortuneScore(score: Int) {
dataStore.edit { it[Keys.FORTUNE_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 markUpdateNoticeDontShow(version: String) {
dataStore.edit { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] = version }
}
Expand All @@ -204,17 +80,4 @@ class UserPreferences @Inject constructor(
suspend fun clearUserData() {
dataStore.edit { it.clear() }
}

suspend fun clearFortuneData() {
dataStore.edit { pref ->
pref.remove(Keys.FORTUNE_ID)
pref.remove(Keys.FORTUNE_DATE_EPOCH)
pref.remove(Keys.FORTUNE_IMAGE_ID)
pref.remove(Keys.FORTUNE_SCORE)
pref.remove(Keys.FORTUNE_SEEN)
pref.remove(Keys.FORTUNE_TOOLTIP_SHOWN)
pref.remove(Keys.FORTUNE_CREATING)
pref.remove(Keys.FORTUNE_FAILED)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ interface FortuneLocalDataSource {

val fortuneCreateStatusFlow: Flow<FortuneCreateStatus>

suspend fun markFortuneCreating()
suspend fun markFortuneCreated(fortuneId: Long)
suspend fun markFortuneFailed()
suspend fun markFortuneCreating(attemptId: String, leaseMillis: Long)
suspend fun markFortuneCreated(attemptId: String, fortuneId: Long)
suspend fun markFortuneFailed(attemptId: String)
suspend fun markFortuneSeen()
suspend fun markFortuneTooltipShown()
suspend fun saveFortuneImageId(imageResId: Int)
Expand Down
Loading