Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.core.net.toUri
import com.yapp.alarm.AlarmConstants
import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.model.MissionType
import com.yapp.domain.repository.FortuneRepository
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -45,31 +46,51 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity)
CoroutineScope(Dispatchers.Main).launch {
try {
if (!hasValidMissionData) {
val hasUnseenFortune = withContext(Dispatchers.IO) {
fortuneRepository.hasUnseenFortuneFlow.first()
val (fortuneCreateStatus, hasUnseenFortune) = withContext(Dispatchers.IO) {
val status = fortuneRepository.fortuneCreateStatusFlow.first()
val unseen = fortuneRepository.hasUnseenFortuneFlow.first()
status to unseen
}
if (hasUnseenFortune) {
context?.let { ctx ->
val uri = "orbitapp://fortune".toUri()
val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage(ctx.packageName)

when (fortuneCreateStatus) {
is FortuneCreateStatus.Creating -> {
context?.let { ctx ->
val uri = "orbitapp://fortune".toUri()
val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage(ctx.packageName)
}
ctx.startActivity(fortuneIntent)
}
ctx.startActivity(fortuneIntent)
}
}
return@launch
}

context?.let { ctx ->
val uriString =
"orbitapp://mission?notificationId=$notificationId&missionType=${missionType.value}&missionCount=$missionCount"
val missionIntent =
Intent(Intent.ACTION_VIEW, uriString.toUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage(ctx.packageName)
is FortuneCreateStatus.Success -> {
if (hasUnseenFortune) {
context?.let { ctx ->
val uri = "orbitapp://fortune".toUri()
val fortuneIntent =
Intent(Intent.ACTION_VIEW, uri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage(ctx.packageName)
}
ctx.startActivity(fortuneIntent)
}
}
}
ctx.startActivity(missionIntent)

FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { }
}
} else {
context?.let { ctx ->
val uriString =
"orbitapp://mission?notificationId=$notificationId&missionType=${missionType.value}&missionCount=$missionCount"
val missionIntent =
Intent(Intent.ACTION_VIEW, uriString.toUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage(ctx.packageName)
}
ctx.startActivity(missionIntent)
}
}
} finally {
pending.finish()
Expand Down
14 changes: 0 additions & 14 deletions core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,6 @@ class UserPreferences @Inject constructor(
}
}

suspend fun tryMarkFortuneCreating(): Boolean {
var canStart = false
dataStore.edit { pref ->
val hasTodayFortune = pref[Keys.FORTUNE_DATE] == today() && pref[Keys.FORTUNE_ID] != null
val creating = pref[Keys.FORTUNE_CREATING] ?: false
if (!hasTodayFortune && !creating) {
pref[Keys.FORTUNE_CREATING] = true
pref[Keys.FORTUNE_FAILED] = false
canStart = true
}
}
return canStart
}

suspend fun markFortuneCreating() {
dataStore.edit { pref ->
pref[Keys.FORTUNE_CREATING] = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.yapp.data.local.datasource

import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.Flow

interface FortuneLocalDataSource {
Expand All @@ -9,11 +10,10 @@ interface FortuneLocalDataSource {
val fortuneScoreFlow: Flow<Int?>
val hasUnseenFortuneFlow: Flow<Boolean>
val shouldShowFortuneToolTipFlow: Flow<Boolean>
val isFortuneCreatingFlow: Flow<Boolean>
val isFortuneFailedFlow: Flow<Boolean>
val isFirstAlarmDismissedTodayFlow: Flow<Boolean>

suspend fun tryMarkFortuneCreating(): Boolean
val fortuneCreateStatusFlow: Flow<FortuneCreateStatus>

suspend fun markFortuneCreating()
suspend fun markFortuneCreated(fortuneId: Long)
suspend fun markFortuneFailed()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.yapp.data.local.datasource

import com.yapp.datastore.UserPreferences
import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.inject.Inject

class FortuneLocalDataSourceImpl @Inject constructor(
Expand All @@ -13,13 +18,23 @@ class FortuneLocalDataSourceImpl @Inject constructor(
override val fortuneScoreFlow = userPreferences.fortuneScoreFlow
override val hasUnseenFortuneFlow = userPreferences.hasUnseenFortuneFlow
override val shouldShowFortuneToolTipFlow = userPreferences.shouldShowFortuneToolTipFlow
override val isFortuneCreatingFlow = userPreferences.isFortuneCreatingFlow
override val isFortuneFailedFlow = userPreferences.isFortuneFailedFlow
override val isFirstAlarmDismissedTodayFlow = userPreferences.isFirstAlarmDismissedTodayFlow

override suspend fun tryMarkFortuneCreating(): Boolean {
return userPreferences.tryMarkFortuneCreating()
}
override val fortuneCreateStatusFlow = combine(
userPreferences.fortuneIdFlow,
userPreferences.fortuneDateFlow,
userPreferences.isFortuneCreatingFlow,
userPreferences.isFortuneFailedFlow,
) { fortuneId, fortuneDate, isCreating, isFailed ->
when {
isFailed -> FortuneCreateStatus.Failure
isCreating -> FortuneCreateStatus.Creating
fortuneId != null && fortuneDate == today() -> FortuneCreateStatus.Success(fortuneId)
else -> FortuneCreateStatus.Idle
}
}.distinctUntilChanged()

private fun today(): String = LocalDate.now().format(DateTimeFormatter.ISO_DATE)

override suspend fun markFortuneCreating() {
userPreferences.markFortuneCreating()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.yapp.data.local.datasource.FortuneLocalDataSource
import com.yapp.data.remote.datasource.FortuneDataSource
import com.yapp.data.remote.dto.response.toDomain
import com.yapp.domain.model.Fortune
import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.repository.FortuneRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
Expand All @@ -19,11 +20,10 @@ class FortuneRepositoryImpl @Inject constructor(
override val fortuneScoreFlow: Flow<Int?> = fortuneLocalDataSource.fortuneScoreFlow
override val hasUnseenFortuneFlow: Flow<Boolean> = fortuneLocalDataSource.hasUnseenFortuneFlow
override val shouldShowFortuneToolTipFlow: Flow<Boolean> = fortuneLocalDataSource.shouldShowFortuneToolTipFlow
override val isFortuneCreatingFlow: Flow<Boolean> = fortuneLocalDataSource.isFortuneCreatingFlow
override val isFortuneFailedFlow: Flow<Boolean> = fortuneLocalDataSource.isFortuneFailedFlow
override val isFirstAlarmDismissedTodayFlow: Flow<Boolean> = fortuneLocalDataSource.isFirstAlarmDismissedTodayFlow

override suspend fun tryMarkFortuneCreating() = fortuneLocalDataSource.tryMarkFortuneCreating()
override val fortuneCreateStatusFlow: Flow<FortuneCreateStatus> = fortuneLocalDataSource.fortuneCreateStatusFlow

override suspend fun markFortuneAsCreating() = fortuneLocalDataSource.markFortuneCreating()
override suspend fun markFortuneAsCreated(fortuneId: Long) = fortuneLocalDataSource.markFortuneCreated(fortuneId)
override suspend fun markFortuneAsFailed() = fortuneLocalDataSource.markFortuneFailed()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.yapp.domain.model

sealed class FortuneCreateStatus {
data object Idle : FortuneCreateStatus()
data object Creating : FortuneCreateStatus()
data class Success(val fortuneId: Long) : FortuneCreateStatus()
data object Failure : FortuneCreateStatus()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.yapp.domain.repository

import com.yapp.domain.model.Fortune
import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.Flow

interface FortuneRepository {
Expand All @@ -10,11 +11,10 @@ interface FortuneRepository {
val fortuneScoreFlow: Flow<Int?>
val hasUnseenFortuneFlow: Flow<Boolean>
val shouldShowFortuneToolTipFlow: Flow<Boolean>
val isFortuneCreatingFlow: Flow<Boolean>
val isFortuneFailedFlow: Flow<Boolean>
val isFirstAlarmDismissedTodayFlow: Flow<Boolean>

suspend fun tryMarkFortuneCreating(): Boolean
val fortuneCreateStatusFlow: Flow<FortuneCreateStatus>

suspend fun markFortuneAsCreating()
suspend fun markFortuneAsCreated(fortuneId: Long)
suspend fun markFortuneAsFailed()
Expand Down
28 changes: 14 additions & 14 deletions feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import android.app.Application
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.lifecycle.ViewModel
import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.repository.FortuneRepository
import com.yapp.fortune.page.toFortunePages
import com.yapp.media.decoder.ImageUtils
import com.yapp.media.storage.ImageSaver
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.ContainerHost
Expand Down Expand Up @@ -50,22 +51,21 @@ class FortuneViewModel @Inject constructor(
}

private fun observeFortune() = intent {
combine(
fortuneRepository.fortuneIdFlow,
fortuneRepository.isFirstAlarmDismissedTodayFlow,
fortuneRepository.isFortuneCreatingFlow,
) { fortuneId: Long?, isFirstAlarmDismissedToday: Boolean, isCreating: Boolean ->
Triple(fortuneId, isFirstAlarmDismissedToday, isCreating)
}.collect { (fortuneId, isFirstAlarmDismissedToday, isCreating) ->
when {
isCreating -> {
fortuneRepository.fortuneCreateStatusFlow.collect { status ->
when (status) {
is FortuneCreateStatus.Creating -> {
reduce { state.copy(isLoading = true) }
}
fortuneId != null -> {
fetchAndUpdateFortune(fortuneId, isFirstAlarmDismissedToday)

is FortuneCreateStatus.Success -> {
fetchAndUpdateFortune(
fortuneId = status.fortuneId,
isFirstAlarmDismissedToday = fortuneRepository.isFirstAlarmDismissedTodayFlow.first(),
)
}
else -> {
reduce { state.copy(isLoading = false) }

is FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> {
postSideEffect(FortuneContract.SideEffect.NavigateToHome)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.repository.FortuneRepository
import com.yapp.domain.repository.UserInfoRepository
import dagger.assisted.Assisted
Expand All @@ -20,35 +21,43 @@ class PostFortuneWorker @AssistedInject constructor(
) : CoroutineWorker(appContext, params) {

override suspend fun doWork(): Result {
val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first()
if (hasUnseenFortune) return Result.success()

val acquired = fortuneRepository.tryMarkFortuneCreating()
if (!acquired) return Result.success()

val userId = userInfoRepository.userIdFlow.firstOrNull()
?: run {
fortuneRepository.markFortuneAsFailed()
return Result.failure()
when (fortuneRepository.fortuneCreateStatusFlow.first()) {
is FortuneCreateStatus.Creating,
is FortuneCreateStatus.Success,
-> {
return Result.success()
}
FortuneCreateStatus.Failure,
FortuneCreateStatus.Idle,
-> {
val userId = userInfoRepository.userIdFlow.firstOrNull()
?: run {
// 사용자 없으면 실패 상태 표시 후 실패 반환
fortuneRepository.markFortuneAsFailed()
return Result.failure()
}

return try {
fortuneRepository.markFortuneAsCreating()
val result = fortuneRepository.postFortune(userId)
result.fold(
onSuccess = { fortune ->
fortuneRepository.markFortuneAsCreated(fortune.id)
fortuneRepository.saveFortuneScore(fortune.avgFortuneScore)
Result.success()
},
onFailure = { e ->
return try {
fortuneRepository.markFortuneAsCreating()

val result = fortuneRepository.postFortune(userId)
result.fold(
onSuccess = { fortune ->
fortuneRepository.markFortuneAsCreated(fortune.id)
fortuneRepository.saveFortuneScore(fortune.avgFortuneScore)
Result.success()
},
onFailure = {
fortuneRepository.markFortuneAsFailed()
// WM 백오프 규칙에 따라 재시도
Result.retry()
},
)
} catch (_: Throwable) {
fortuneRepository.markFortuneAsFailed()
Result.retry()
},
)
} catch (_: Throwable) {
fortuneRepository.markFortuneAsFailed()
Result.retry()
}
}
}
}
}
17 changes: 10 additions & 7 deletions feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent
import com.yapp.analytics.AnalyticsEvent
import com.yapp.analytics.AnalyticsHelper
import com.yapp.domain.MissionMode
import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.model.MissionType
import com.yapp.domain.repository.FortuneRepository
import com.yapp.media.haptic.HapticFeedbackManager
Expand Down Expand Up @@ -133,13 +134,15 @@ class MissionViewModel @Inject constructor(
performHapticSuccess()
logMissionSuccess(type)
if (state.missionMode == MissionMode.REAL) {
val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first()
val isFortuneCreating = fortuneRepository.isFortuneCreatingFlow.first()

if (hasUnseenFortune || isFortuneCreating) {
postSideEffect(MissionContract.SideEffect.NavigateToFortune)
} else {
postSideEffect(MissionContract.SideEffect.NavigateBack)
val fortuneCreateStaus = fortuneRepository.fortuneCreateStatusFlow.first()

when (fortuneCreateStaus) {
is FortuneCreateStatus.Creating, is FortuneCreateStatus.Success -> {
postSideEffect(MissionContract.SideEffect.NavigateToFortune)
}
FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> {
postSideEffect(MissionContract.SideEffect.NavigateBack)
}
}
} else {
postSideEffect(MissionContract.SideEffect.NavigateBack)
Expand Down