diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d56d2894..6c350ede 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { android { namespace = "com.yapp.orbit" + compileSdk = 35 defaultConfig { versionCode = 6 @@ -53,4 +54,6 @@ dependencies { implementation(libs.firebase.crashlytics) implementation(libs.play.services.ads) implementation(libs.kotlin.reflect) + implementation(libs.hilt.worker) + implementation(libs.androidx.work.runtime) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 780b61bb..6eabb63b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,9 +46,7 @@ - + @@ -80,5 +78,15 @@ + + + + diff --git a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt index 7391cf6e..b06538f8 100644 --- a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt +++ b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt @@ -1,13 +1,24 @@ package com.yapp.orbit import android.app.Application +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration import com.google.android.gms.ads.MobileAds import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject @HiltAndroidApp -class OrbitApplication : Application() { +class OrbitApplication() : Application(), Configuration.Provider { + + @Inject lateinit var workerFactory: HiltWorkerFactory + override fun onCreate() { super.onCreate() MobileAds.initialize(this) } + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() } diff --git a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt index e266704f..15db09ff 100644 --- a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt +++ b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt @@ -14,7 +14,9 @@ internal fun Project.configureHiltAndroid() { dependencies { "implementation"(libs.findLibrary("hilt.android").get()) "ksp"(libs.findLibrary("hilt.android.compiler").get()) + "ksp"(libs.findLibrary("androidx-hilt-compiler").get()) "implementation"(libs.findLibrary("hilt-navigation-compose").get()) + "implementation"(libs.findLibrary("hilt-worker").get()) } } diff --git a/build-logic/src/main/java/orbit.android.feature.gradle.kts b/build-logic/src/main/java/orbit.android.feature.gradle.kts index aa5803c4..47d5c072 100644 --- a/build-logic/src/main/java/orbit.android.feature.gradle.kts +++ b/build-logic/src/main/java/orbit.android.feature.gradle.kts @@ -1,4 +1,3 @@ -import com.yapp.convention.configureHiltAndroid import com.yapp.convention.libs plugins { @@ -6,8 +5,6 @@ plugins { id("orbit.android.compose") } -configureHiltAndroid() - dependencies { implementation(project(":core:designsystem")) implementation(project(":core:ui")) diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt index 39de3171..8d2389b7 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt @@ -11,10 +11,9 @@ import com.yapp.domain.repository.FortuneRepository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import java.time.LocalDate -import java.time.format.DateTimeFormatter +import kotlinx.coroutines.withContext import javax.inject.Inject @AndroidEntryPoint @@ -42,24 +41,38 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity) missionCount != -1 ) - if (!hasValidMissionData) return - - CoroutineScope(Dispatchers.IO).launch { - val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - - val shouldLaunchMission = fortuneDate != todayDate + val pending = goAsync() + CoroutineScope(Dispatchers.Main).launch { + try { + if (!hasValidMissionData) { + val hasUnseenFortune = withContext(Dispatchers.IO) { + fortuneRepository.hasUnseenFortuneFlow.first() + } + 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) + } + } + return@launch + } - if (shouldLaunchMission) { - context?.let { + 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(it.packageName) - } - it.startActivity(missionIntent) + val missionIntent = + Intent(Intent.ACTION_VIEW, uriString.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(ctx.packageName) + } + ctx.startActivity(missionIntent) } + } finally { + pending.finish() } } } diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt index edd18466..011dadef 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt @@ -12,6 +12,7 @@ import com.yapp.alarm.services.AlarmService import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper import com.yapp.domain.model.Alarm +import com.yapp.domain.model.toAlarmDay import com.yapp.domain.model.toTimeString import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.usecase.AlarmUseCase @@ -19,8 +20,8 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -105,17 +106,31 @@ class AlarmReceiver : BroadcastReceiver() { androidAlarmScheduler.cancelSnoozedAlarm(notificationId) context.stopService(alarmServiceIntent) - sendBroadCastToCloseAlarmInteractionActivity( - context = context, - notificationId = notificationId, - missionType = missionType, - missionCount = missionCount, - ) - CoroutineScope(Dispatchers.IO).launch { - val alarms = alarmUseCase.getAllAlarms().first().sortedBy { it.isAlarmActive } - val isFirstAlarm = alarms.firstOrNull()?.id == notificationId + val alarms = alarmUseCase.getAllAlarms().first() + + val isSnoozeId = notificationId >= AlarmConstants.SNOOZE_ID_OFFSET + + fun Alarm.ringsToday(): Boolean { + if (repeatDays == 0) return true + + val todayAlarmDay = LocalDate.now().dayOfWeek.toAlarmDay() + return (repeatDays and todayAlarmDay.bitValue) != 0 + } + val earliestIdToday: Long? = alarms + .asSequence() + .filter { (it.isAlarmActive || it.id == notificationId) && it.ringsToday() } + .sortedWith(compareBy({ it.hour }, { it.minute }, { it.second })) + .firstOrNull() + ?.id + + val isEarliestAlarmDismissedToday = + !isSnoozeId && (earliestIdToday == notificationId) + + if (isEarliestAlarmDismissedToday) fortuneRepository.markFirstAlarmDismissedToday() + + val isFirstAlarm = earliestIdToday == notificationId analyticsHelper.logEvent( AnalyticsEvent( type = "alarm_dismiss", @@ -126,12 +141,12 @@ class AlarmReceiver : BroadcastReceiver() { ), ) - val existingId = fortuneRepository.firstDismissedAlarmIdFlow.firstOrNull() - if (existingId == null) { - fortuneRepository.saveFirstDismissedAlarmId(notificationId) - } else if (existingId != notificationId) { - fortuneRepository.clearDismissedAlarmId() - } + sendBroadCastToCloseAlarmInteractionActivity( + context = context, + notificationId = notificationId, + missionType = missionType, + missionCount = missionCount, + ) } Toast.makeText(context, "알람이 해제되었어요", Toast.LENGTH_SHORT).show() diff --git a/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt b/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt new file mode 100644 index 00000000..e16e8dd4 --- /dev/null +++ b/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt @@ -0,0 +1,5 @@ +package com.yapp.alarm.scheduler + +interface PostFortuneTaskScheduler { + fun enqueueOnceForToday() +} diff --git a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt index 96596729..56141679 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt @@ -23,10 +23,10 @@ import com.yapp.alarm.pendingIntent.interaction.createAlarmAlertPendingIntent import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissPendingIntent import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozePendingIntent import com.yapp.alarm.pendingIntent.interaction.createNavigateToMissionPendingIntent +import com.yapp.alarm.scheduler.PostFortuneTaskScheduler import com.yapp.domain.model.Alarm import com.yapp.domain.model.AlarmDay import com.yapp.domain.model.MissionType -import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.usecase.AlarmUseCase import com.yapp.media.sound.SoundPlayer import dagger.hilt.android.AndroidEntryPoint @@ -34,10 +34,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import java.time.LocalDate -import java.time.format.DateTimeFormatter import javax.inject.Inject @AndroidEntryPoint @@ -55,7 +52,7 @@ class AlarmService : Service() { lateinit var androidAlarmScheduler: AndroidAlarmScheduler @Inject - lateinit var fortuneRepository: FortuneRepository + lateinit var postFortuneTaskScheduler: PostFortuneTaskScheduler private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -82,7 +79,7 @@ class AlarmService : Service() { super.onDestroy() } - private suspend fun handleIntent(intent: Intent) { + private fun handleIntent(intent: Intent) { val alarm: Alarm? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(AlarmConstants.EXTRA_ALARM, Alarm::class.java) } else { @@ -124,16 +121,14 @@ class AlarmService : Service() { if (isOneTimeAlarm) { turnOffAlarm(alarmId = notificationId) } + + postFortuneTaskScheduler.enqueueOnceForToday() } - private suspend fun shouldNavigateToMission( + private fun shouldNavigateToMission( missionType: MissionType, ): Boolean { - if (missionType == MissionType.NONE) return false - - val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - return fortuneDate != todayDate + return missionType != MissionType.NONE } private fun createNotification(alarm: Alarm, shouldNavigateToMission: Boolean): Notification { diff --git a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt index 35298556..220dfa55 100644 --- a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt +++ b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt @@ -1,6 +1,5 @@ package com.yapp.datastore -import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey @@ -26,15 +25,22 @@ class UserPreferences @Inject constructor( val USER_ID = longPreferencesKey("user_id") val USER_NAME = stringPreferencesKey("user_name") val ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed") + val FORTUNE_ID = longPreferencesKey("fortune_id") val FORTUNE_DATE = stringPreferencesKey("fortune_date") val FORTUNE_IMAGE_ID = intPreferencesKey("fortune_image_id") val FORTUNE_SCORE = intPreferencesKey("fortune_score") - val FORTUNE_CHECKED = booleanPreferencesKey("fortune_checked") - val FIRST_DISMISSED_ALARM_ID = longPreferencesKey("first_dismissed_alarm_id") - val DISMISSED_DATE = stringPreferencesKey("dismissed_date") + 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 = stringPreferencesKey("first_alarm_dismissed_date") } + private fun today(): String = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + val userIdFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } .map { it[Keys.USER_ID] } @@ -70,108 +76,150 @@ class UserPreferences @Inject constructor( .map { it[Keys.FORTUNE_SCORE] } .distinctUntilChanged() - val hasNewFortuneFlow: Flow = dataStore.data + val hasUnseenFortuneFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } - .map { preferences -> - val savedDate = preferences[Keys.FORTUNE_DATE] - val isChecked = preferences[Keys.FORTUNE_CHECKED] ?: true - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - savedDate == todayDate && !isChecked + .map { pref -> + pref[Keys.FORTUNE_DATE] == today() && + pref[Keys.FORTUNE_ID] != null && + (pref[Keys.FORTUNE_SEEN] != true) } .distinctUntilChanged() - val firstDismissedAlarmIdFlow: Flow = dataStore.data + val shouldShowFortuneToolTipFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } - .map { preferences -> - val savedDate = preferences[Keys.DISMISSED_DATE] - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - - if (savedDate == todayDate) { - preferences[Keys.FIRST_DISMISSED_ALARM_ID] - } else { - null - } + .map { pref -> + val hasTodayFortune = pref[Keys.FORTUNE_DATE] == today() && pref[Keys.FORTUNE_ID] != null + val tooltipNotShown = pref[Keys.FORTUNE_TOOLTIP_SHOWN] ?: false + hasTodayFortune && !tooltipNotShown + } + .distinctUntilChanged() + + val isFortuneCreatingFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.FORTUNE_CREATING] ?: false } + .distinctUntilChanged() + + val isFortuneFailedFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.FORTUNE_FAILED] ?: false } + .distinctUntilChanged() + + val isFirstAlarmDismissedTodayFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { pref -> + val flag = pref[Keys.FIRST_ALARM_DISMISSED_TODAY] ?: false + val date = pref[Keys.FIRST_ALARM_DISMISSED_DATE] + flag && date == today() } .distinctUntilChanged() suspend fun saveUserId(userId: Long) { - dataStore.edit { preferences -> - preferences[Keys.USER_ID] = userId + dataStore.edit { pref -> + pref[Keys.USER_ID] = userId } } suspend fun saveUserName(userName: String) { - dataStore.edit { preferences -> - preferences[Keys.USER_NAME] = userName + dataStore.edit { pref -> + pref[Keys.USER_NAME] = userName } } - suspend fun saveFortuneId(fortuneId: Long) { - val currentDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_ID] = fortuneId - preferences[Keys.FORTUNE_DATE] = currentDate - preferences[Keys.FORTUNE_CHECKED] = false + suspend fun setOnboardingCompleted() { + dataStore.edit { pref -> + pref[Keys.ONBOARDING_COMPLETED] = true } } - suspend fun markFortuneAsChecked() { - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_CHECKED] = true + 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 saveFortuneImageId(imageResId: Int) { - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_IMAGE_ID] = imageResId + suspend fun markFortuneCreating() { + dataStore.edit { pref -> + pref[Keys.FORTUNE_CREATING] = true + pref[Keys.FORTUNE_FAILED] = false } } - suspend fun saveFortuneScore(score: Int) { - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_SCORE] = score + suspend fun markFortuneCreated(fortuneId: Long) { + dataStore.edit { pref -> + val isNewForToday = pref[Keys.FORTUNE_ID] != fortuneId || pref[Keys.FORTUNE_DATE] != today() + + pref[Keys.FORTUNE_ID] = fortuneId + pref[Keys.FORTUNE_DATE] = 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 saveFirstDismissedAlarmId(alarmId: Long) { - dataStore.edit { preferences -> - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - if (preferences[Keys.FIRST_DISMISSED_ALARM_ID] == null) { - preferences[Keys.FIRST_DISMISSED_ALARM_ID] = alarmId - preferences[Keys.DISMISSED_DATE] = todayDate - Log.d("UserPreferences", "첫 해제된 알람 ID 저장 완료: $alarmId (날짜: $todayDate)") - } else { - Log.d("UserPreferences", "이미 첫 알람 해제 ID가 저장되어 있음)") - } + suspend fun markFortuneFailed() { + dataStore.edit { pref -> + pref[Keys.FORTUNE_CREATING] = false + pref[Keys.FORTUNE_FAILED] = true } } - suspend fun setOnboardingCompleted() { - dataStore.edit { preferences -> - preferences[Keys.ONBOARDING_COMPLETED] = true + suspend fun markFortuneSeen() { + dataStore.edit { pref -> + pref[Keys.FORTUNE_SEEN] = true } } - suspend fun clearDismissedAlarmId() { - dataStore.edit { preferences -> - preferences.remove(Keys.FIRST_DISMISSED_ALARM_ID) - preferences.remove(Keys.DISMISSED_DATE) + suspend fun markFortuneTooltipShown() { + dataStore.edit { pref -> + pref[Keys.FORTUNE_TOOLTIP_SHOWN] = true } } - suspend fun clearUserData() { - dataStore.edit { preferences -> - preferences.clear() + suspend fun saveFortuneImageId(imageResId: Int) { + dataStore.edit { pref -> + pref[Keys.FORTUNE_IMAGE_ID] = imageResId } } - suspend fun clearFortuneId() { - dataStore.edit { preferences -> - preferences.remove(Keys.FORTUNE_ID) - preferences.remove(Keys.FORTUNE_DATE) - preferences.remove(Keys.FORTUNE_IMAGE_ID) - preferences.remove(Keys.FORTUNE_SCORE) - preferences.remove(Keys.FORTUNE_CHECKED) + suspend fun saveFortuneScore(score: Int) { + dataStore.edit { pref -> + pref[Keys.FORTUNE_SCORE] = score + } + } + + suspend fun markFirstAlarmDismissedToday() { + dataStore.edit { pref -> + pref[Keys.FIRST_ALARM_DISMISSED_TODAY] = true + pref[Keys.FIRST_ALARM_DISMISSED_DATE] = today() + } + } + + suspend fun clearUserData() { + dataStore.edit { pref -> pref.clear() } + } + + suspend fun clearFortuneData() { + dataStore.edit { pref -> + pref.remove(Keys.FORTUNE_ID) + pref.remove(Keys.FORTUNE_DATE) + 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) } } } diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png deleted file mode 100644 index d30beec6..00000000 Binary files a/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png new file mode 100644 index 00000000..c75d2db3 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png new file mode 100644 index 00000000..a119387d Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png deleted file mode 100644 index 469a7b3f..00000000 Binary files a/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png new file mode 100644 index 00000000..318d11d6 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png new file mode 100644 index 00000000..83b665f4 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable/ic_100_buble.png b/core/designsystem/src/main/res/drawable/ic_100_buble.png deleted file mode 100644 index e69978c4..00000000 Binary files a/core/designsystem/src/main/res/drawable/ic_100_buble.png and /dev/null differ diff --git a/core/designsystem/src/main/res/raw/fortune_loading.json b/core/designsystem/src/main/res/raw/fortune_loading.json new file mode 100644 index 00000000..cb570812 --- /dev/null +++ b/core/designsystem/src/main/res/raw/fortune_loading.json @@ -0,0 +1 @@ +{"nm":"컴포지션 2","h":500,"w":700,"meta":{"g":"@lottiefiles/toolkit-js 0.66.4","tc":"#202f44"},"layers":[{"ty":2,"nm":"Group 1948760243.png","sr":1,"st":1,"op":45,"ip":0,"ln":"2856","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.5,137]},"s":{"a":0,"k":[61.6,61.6,101.65]},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[130.5,159,0],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.139,178.955,0],"t":15},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[233.8,193.529,0],"t":31},{"s":[285.485,192.267,0],"t":45}]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[17.4],"t":0},{"s":[32.4],"t":45}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"1","ind":1},{"ty":2,"nm":"Group 1948760248-1.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2855","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[22,121.064]},"s":{"a":0,"k":[70.27,70.27,89.655]},"p":{"a":0,"k":[386,148,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[-9.304],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":45},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[10],"t":48},{"s":[0],"t":51}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"2","ind":2},{"ty":2,"nm":"Vector 27856.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2854","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[93.94,9.052]},"s":{"a":0,"k":[75.628,72.572,100]},"p":{"a":0,"k":[310.5,242.5,0]},"r":{"a":0,"k":2},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"3","ind":3},{"ty":2,"nm":"Group 1948760248.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2853","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[132.5,232]},"s":{"a":0,"k":[76.4,76.4,76.4]},"p":{"a":0,"k":[370,256,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"4","ind":4}],"v":"5.7.0","fr":30,"op":45,"ip":0,"assets":[{"id":"1","e":1,"w":303,"h":274,"p":"data:image/webp;base64,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","u":""},{"id":"2","e":1,"w":100,"h":148,"p":"data:image/webp;base64,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","u":""},{"id":"3","e":1,"w":154,"h":124,"p":"data:image/webp;base64,UklGRuAFAABXRUJQVlA4WAoAAAAQAAAAmQAAewAAQUxQSKECAAABoFZbjxbNeiSUhEgoCSUBB+Cg2wE4GByAA14HkYCESCgJmZk7Xwn19P0bEROA/5GrHq9lauUXbZvlV+UDmPy3tx5Hd3fXfdfubod27zoLZLV+TOVXUgGUX4gM7vxdwlv1Nne3l7qti7q7He13ZZpkLOWbP97217JMbTJ3m6Wp2/yf8rr02uqvantANR/yte1X919267qpu03rLqnm7lR7JnG2mfRzEf9cTjo9z+J0NU3tfI405nznLIsTliTNCB3IOTlhkyQ3I0FSJ2zI2ZSR5hCnvOd4c5pzbJwkRbkpKVJeTnlLsTjndwYxUi2BmHM2JDQnPScQJ22SYGF1IKGxaglWJ20IL9+c9RTWzFlviBbn3cI2YvWDQfjF6w5bnfccVZ23IdqIbVGTfy4nszno5cwlpjrzHbHGTEvM6sS/ELs68z1GnPocU6kZYgu1OUaUmSJWnXiXICfeK2ILsxnBjVhHtBI7ohbnbRJUjNiM4NV5G4LFiWuQ2OdizvwrZnXqc4g49QuhjZpJjDDrgtjKbEawETMEN+fdJeokNiO6E5Mw562IFmI17OS1I1qc9o7whdYb8RsrRcKT1TvDzaomWJy0Ib4YK0nQnPSBhJWVZCikdmSsnKykWCiZIOVFqG9I2vmYIKvznZHW+MjnsiHt4mw35FUy1pD44nIh9ZuL5CpUvpBcmczZGpEd6ZWEtYL8bw4meKJQuAXPNAJ7wUNrH12f8Fyxse0FT64j6w0P14HNeHobl+H5NiqTAYiNSQUjlD6afh+vhkG+B7NhqNtQDgxWlj4Mk9EA1QahggHL+Tjd9mPCoGt/ki0FQ6/2HBMMv54P6Md1awVDOa2n0glkRbNoA99yBvTrOK6729akgHNprUptr+t275e5+/2LvYAgAFZQOCAYAwAAsBUAnQEqmgB8AD5tNJRIJCKiISew2ZCADYlN26vDq+ft/arZo7r+V/5AdAHuX4Kw2j8z9vXaA8wD9QekB5gP2V/YD3kvQB/kfsA+QD+q/5DrAPQA/Z30x/2V+CD9t/3M+A39fv/jnFf8A/Br6/e/wnFywDKjMbd3pp6oeu2XL9y8Rgfai6GXfCl10vW2a4ZnteRP7+IQClwq7HGXe09pjD9T+rOUKow07j+uvJuQ8TAR6pOaMAAA/vz4Tu/BRBrBWfRzuE4cYNbsibMnNJVhv/dV7HywuKZWsfvtgQgHT/IN6KNBvgyJhTNUeBnIqKwuDYmRXWxn4fqgRkf/9/i/NO7sHbbC6BF7hoyXFeX5Kd7jDB4AxtMNaf6E3YSFFeXIsf/ShdnGethZi+TsZEdw7Z7P1KV+DTuP3F1+sbN/XQ2GBlVip0fY8U7/lMe50AAA+XLZtQ6BVS76BAnc4fpsb8mcB5WPaYczxcrRb9klf4n4xmzKdpkQv7Yxh8f/8Yd2s+OC/pfdJHnVXMeNeFoJygAktddbb1rqpDEBX9D27XEsPn7Tcwe1Nv/7Qe5rlMWl4n+N0c9jlYuBvR1I57zAtMHCT/1gABYpZvlWqNY1yw7X8insD16IMR1A5FqVriaBeuED/wGTvRUIwAofebI7gmoH538RLtZtIq5xdfrG2zAAcmVZfmtXwrmSvPryWTg4s2L96PYfgs+wlbh//4sEgzf2maHfzrK6xE5fUQvGF6AakxXOG3lde+R/Ji5NxaBGX8wRNEQAqFpvBIaKHe8q1o0ojlfE45xam1T9H4sxqeNNI/6B4bL15Xf47QPH74XG6xUyahl+JTiiJG1/LsCDNugAwPNjZKPcAP/1DdAwB7GfrRnwYir829ce/gVMJDwQ/QmBdUomeGdMjzDJ23rhu9LXRmGhcd4jFJrUuAKKK3+0/86j8+bVQOsV2tHPBnAwmB16k0fRdwmbaALgVVRbFxxUtoVKU5rjGPF9ftoxHl6sEzduQXQOmr13bDNmiHtRmEurhDFjNR5g4BlpLV/tkdBxHofgAAAA","u":""},{"id":"4","e":1,"w":265,"h":464,"p":"data:image/webp;base64,UklGRuYSAABXRUJQVlA4WAoAAAAQAAAACAEAzwEAQUxQSKgIAAABHAVt2zAJf9rdRRAREyClueEBc8tfIG1Z2fYIjRUJIyESkDAODg46DsBBx0FxAA5YB5GAhEiIhP+iPdvuXxi4jIgJkFTbdtg6F8KDIAiGIAY2g4hBwyBmkDCIGbgMBMEQBEEQ3iDp9J/bP4qICdD/51Rmd/dA2O10pWFQEUnDPJtt+LXnM5SGmz6lYgCw1inw0bCbdt3JSH3JT3m1m0rnANZ5dnA+TkGudehEdAEAvxfDcwTIH6nNutwlyQZTSTOeN8f3+6yNlEoW0bv5OgUABJ49sKuP1CKa/pf6UoZqACKw+35LLZDSk+YsImqATX1fap0Dx+r340r5qbcArFoA8HkOHPn9SDSJSJ7Mplv3AHxdA0352LGU04u8Ws1JDZinQMvedygP89TLEAA2s8DLQAPnXdGuSw88BxrcdT+GQOO77kSacQLHHdDBAqfw/m3JcB7vX5XyhjPpt/Qt6RE4m67fUR0n1NMXlCPPaeX7MxOq0T2TayJrCXbkKoOMUZWRaGemnnBXpYnE64nlwQeVI94JeKMoVyLODCMZG0FLyvnPysBkf/ZMzqZ/E0na9U8qKrheBrC/eMBC/twjadvnBi50n4rkPX2qAov0oQMY6mduSdw+siXy+EQZzHD7wCOh2wcuaujeS+zjWxu3+Z0ybLAld09vNHCwNxZyyL97opt+1RK9/aqzQ/5FJPz6i+aDk17+xaAn10G+DqZfdHrzL04b3JJ+pH8MfBi/leRfv2n64LRBSRucBrBv0wD4EmmD8guh+eCyQaQN6i+E8IG6Dw4HbF9OB9y+dAfol+GD6YDHl7SgGRYf/LGASSppwVnS4YEfSacHXNLdA5BUfFB9sHvAfRA+gA9CUvPAj6S7B2ZJuwc2SS8PmKTmgZukaoFQSbsFTCR1C0SSNCyAhw/CB5CUHpylxQRVupsgpG4CSJcPhgk2qZtglnYTZGkzgUjFAy5SeMBEkgd+RFo84Ek6PIAqTROElC60QjdBSNUEP5IOD/yTtHtAJL0ssD0NCyBJS3owS5sPmgdcpOqBSFIMC6BIOjwwSi09qFL3gIl0esBFenkASaomGCV1D5ik5gGXdPfB4QGTIj34I1UT3KSYHlBJuwVmkdQs4ElStwCypNMD/yTtFnCRVCywPW0WQJbUPPBP0uIBlaSXA37k83CAvbgcEC+GA/zF6YD5xcsBjxeXA5A+0oL5ozsg9GNzwCifhw+mA+xFOtBfdAfEi+aA7UV1wO0ymOQaqH0n528rtmb5ZUxiDiDmJO/uuGLqRFTlgxWWZfn8i5JrrrWo/GVH5NuU5O8nmJh61S6XnIQyEqvVJOR2PmKa56mo8KufCd/MJpWvrafBa5IvPwUx1aLy/eMJ2DrZR2040y6riMpedk3mQ9/XLHu7tJip7HK0Vmw1yz4ntHLMQ+k7lR3XZppk/6OV/h1AaaV6AOKNZEeARo4j8EaCHoAuG4BoHpdj1NxJ6qfad1mrAwjfmqMexLu5ZBHRvpTS5WEyIObt+NIhva85iYjmYTH3NedprrUMDgSA8PiOzQHPakS1CT6eVVJWEdFiVgdH1AVwJ4ipJJHcJZGJ6Kdl3u+SSFIRHdbN/XNuN/ltIbKWervray2aS6/9EkDMq4dbkjd7ornh3kwqH85E9Sx8vicaz04h+nd2eqLu7CjR7Tqw6wDXgZ+dTDSfnY5opyNxHaw8A0/lyUpnIWpwCogXOM4ktgnMhY1QVTjBtMCpRFNwUxAFHAVxg1OYdjgLU4WzMgWchegtuCNRoZN5huimoMmgI0Q7nTt4Z8BxoqQTRG/BnYhWOoWo+2D64KLTE210MlHQWYh2OkF00XGiDDiFqcARvwwUxAFnJBqCOxHd6KxEQWchqj6401mJ3nQ2ouGDDDiVqcDpiIbgKtGbTkdU6SSilY4Ez4ln48mFjhGtcBS8o8BZiG6C60SNzkLU6WSiGXCUKCucjqnBWZgqHGda2CiIh2xwgyNBJLrGM/D0PHmjMxJ1OkaUAWdlKnBGpgonMwWcjmjKBhlwMlP1wQKnJ3oL7ki00JmICp2FqP43y9eBEu10CtFfOkbU6WxEdx+cdFaiXOCMTA3Och0408omgbmwESPqgpuJZsCRjScbnYVop7MR3eEoiFc4PdEQ3EL0pFOJug8yfLDC6ZnucBLTDqdcB3YdOFOFMzEFnEzUBVeJdh+cdIRohg1ygVOYVjjG1OCs10G5DiSug+WKWX0QdFaeS3SNZ/og6RTwdjpOVOmAWHSNZwYdDZp80kkgLnB6pgpnYSpwQNzFNl0H4kQXnZUoA44zNTYK5v0Xwo2NBFOBMxJdous8nU4C76BTroOVaNJxoixwjKnBGZlWOImpwBHn6YKr4L3oGNGg40RJx5gKnHzJVDg90+IDwc1E3Qc7nYmo0lmICp2JqNIpRI3OSHSnU4k6nZFoBpxMlAVOug6ESXSdqNAB8eKDoBM8XXS3y0BBvMAZmVY4C9EocEaim2wguitR0AmiBieBuMNRpglHgigLnJGpwtEgCjgLeKfgbkQZcIypwFmYAs5I9BZcJVroyHWgRI1OT1TJpDIHiJ9bwTIE6N+FyYxvHIXIHd85Cg+NL8lxgzE4vviBouC7HyT8y/LB4Y6vv16FQBpW7OHYfn5dYC8fPz117OcoPzp17Olc2i1PgX2d2moP7K81mmGXTRvsgZ0Oba47dntrrRk7Xtvqjl2/tVTGztuQG2kIHKBrC91xkGP73HGY99ZRHOgjNY36kWBrmSFwrLVZkuFoI7XKguOtjaI44GiT5EeErkkWHHJuElwHdkzaJBpHNEub5gNybRSZDsc7adUUBzMladd6DDFvTzFKy6Zl36YyLLZmEcm1liStmzT3peRUX/gObEDUTss8d9LqWktOosNclxfhb8VHHAgPxDSs69BNsWXp+iTnscxTn0SSqnT9UGspi00qWgG4WQAxT7XWkuQwAVZQOCAYCgAAUEcAnQEqCQHQAT5tNpZIpCMiISRQ+TiADYljbuFxHlVfACKxnn/D/mV4jmp/J/339tPyi7STjTwPzDFW+axyZ/zvuN+ZH+K/y3sN8wD9TPO3/WD3Tft1+AHwH/XT/kf7L3f/8J/rv7z7jP6//n/xV+QD+e/33rBv7N/zPYA/h/929MH/1f5X4Jv26/eP4Gf11//3+4/8faAf971APVfyAHdsHsRReiF9iSPRmndPI7Btv4fAxaBQpGRkZEHlGfCvqJAeEtnkvkG/GbHYBvalYHdzSaI//igbCMmxIcf5pob3KGRTLeAD1K1uJSIfR8rXNKZQSJneVJIM3AfLhrnC5neVJM8cV3kcM3lXXHU3DrFBOaYa/pLKzEmHWKCc0w1/YCcx0LqxQtcFthYD5ZQNQGs7ypLf14WFgPmimAAkfP1wzk3AeWo8On66sULXcybFz5apv7XItHBuKT71AAeensC2a/pxAvtjnuiCOk7lXwAIra/AesFK6lX30nCnS/9OqcbWna4uf39d1Na0Eg6feJWX/+OCw56BJCnyxYXzxi6TgkhTAASPkMNtVhh1ihbKqYepKrZFS3gBJCTQcczzTAASQpf6EPLmeaYACSFL/QhMH+WM2FlVYoG2aMhm4dYoWypQR7fC88qW8AJHyGHEgXnlS3gBI+Qvc1mu8qW8AJAXuZ4oqlvACSFJdSU7FFUt4ASQkCQp5KRFjKKrWPwhycFgzOtLeAEj3ltYB75SfEoRcCNZmuYUt4AOd47hr+Ym2QwAAP76OQR8MTnlYDAGkxYTgv4VnW3bCBYpyOasECiT+eZMknjAdumgKP4KY17tMlNsVw+hp857s59ZM1q6+JH2eeVSXK8gzcbGzhSWhxtBMZ52tTtZUW4qOrVt/zeM2+LRt6HzQ5jU/0hkN+VVEm+bGepE3aZOZz8n8GZKRUEWnzIEsMD/zkGDWRi6c2Y/udpFouIupDDX0kS0s37VZ75cgU55JVsXANfQ/xVZbHXNZsELFSLRZV3IxmDe+yfE+hxaqDwyQ35MVbtBpbfqb9ZJUj23FMgw1tW5IXiS6lOcyDJRYgFj7XkEbixFW8V/0afFGUs4rKkStEf3SDCqw+v2esJoSpikKwch1pb/IGCRATHk939e//9We//2pH+xZjQWIUBlhaUPPmVfeiln/RlNVtl/nBFLlwCDZbPeLN19xZkBrexEEpihT4K0QsSAACnHgPKmIFzxjy60EYL0PwAAa3WwKdcANpVgVdGjeCN/8zAeCu2tIdRYDP3/wT4vWznwMquJxtx+21IwUDuQg1c2Yo6NzD0yY4o+bqK4IeJoo37IZDY0qAOARoGi4N2vjOCbHCCpzauTZrw1fXqPlB7HOGIspr5fZbkq7ZC/+gegU1AjHVP4Wf3cI9i8XFzj6H6HgXai65n6HZ2kd98ecPlmy2GsBBvjNMB1p2DILR0y0AdsomvSMobR9Ih6gOgITkffjxSsjwVwAFgYH7ondvcMjFZ0yAsgs0AXp6dwwesi2l0tmL7u4g5332n4Lbq2wdt+JxbVOPOuW70z9/DqB4RdjBGWaohsBh5+vY3LloWhflPRnI/RS6zrOiEKA3iLlh6gciN5QWpcAEaV/lQ6vhukMofgYPPZBDs+acyy7m7IzlAWVmlt+nCO3sh5bI3z01nRZ69vP0m8pDih4U9pvUOdSj1AFjb0SME/2YNdq79hmweMQR1KmF9kRDYlDrbqN8tXZmlXoB6lEmoIFgTZWQ7q3tgWqgbYYLkQUMAC6FJsZSMOBXmJ88vibkIAPmzfWMIF6UKwoZ+h5yLEDw3nGp9p39HjeZ9CH9IH25xGGSVyxM80mgTaU4VWHGg7D6+M6np+IfiC4w7kAGHzZYvNnxWlh1+3wzbJB0My23elGlh8bgw8/cttSHe5Fso80M77zzdCzL4fJVlqMe33/O2/Q1wDG6+owuKFALlJ/Bi62Vf2sFYykkc0jtKt7A/EFEXV56RE9ZkLV3EoaolhN/lT43X/fRhs7NoQYRlphfDCdptPfuyYMDe/KoPIXEBkDh/7AhgIhT3H/J0pRncDvs/RbQgOqNDMNjsWUU8KmZRjOatCLCzZfRWlvbaBNvtoBr7Pu9dDfg33qvtoHykVz9ym776qHojKYp/Q5VEcDbIcQbgIMuNPIqr+ApOg4+fJjWH9Rap+Ml3TyZN6eJEOcfSA1ZTlvzldmhENCkznH/hFtOhhpyg905MCACN7i+f//K25E/nN7vG2cemCRXmstpWTZgPjJ8vLF7bUsgMlHdmAgNzddcJ0A6Al1kyG0uoYETRgrecDuyPBVaNWswPj44BWF75PgiiVkGZI7S9wb5e1v//LrOsvtMVmjtYGFJ1oQOSK8JW+766FPh80eBjyBJN8f2vCtcWk0xEWxaj3baxRzvCQJIb8HBiDm+KEmvr77gja9nsBRjN7G2FYiPZc8+AvhTH0wkJ5aRLZlCDZK9OvWtwFkAe48RMcTNXg0nsPR/+nOoV9YAAAAAfQ1EfBAPLXrMhCmWXtAZSTHBnxHAvycfQBGQ14CeY1LtmIy3wR2VxDvZ2jj7mC0MoLpzn2Ll4UbKR7e5rYQuSP1V4oUXsSYxr2tTPMVCfSrAIDxOoxCMj/fdX63sbFt4DB4F6a5Ea6R0SFUNsaEp1mYmnUq+ygAZOtyHpUEgrm9PMikBKFHS2ZFNwGJMKOgDh0V98be2grTcLeUc9tE3g8UbDsNR1gLfzjIgRYxCn2/LuN0P85brOgBnQDufQlNUwAtP4PzNnsa6ztqfXvTpz999l4P3wcB5/ld2OuReeJW4IhejqXkL/Bsx6e/ATF66FSG2TiTmsOjiNoRu0yE436SBzISTew26Sk4bAnEVXP30yAIyvxXHhMPmNAtymdshbg2guAqRYbQXAWyH3JjietQeLFdaCLcFWREaoBoFNm02VZ/GoOwpoZ8RNmGQGqzm2Jo2A99EMenNxM3xaywdplSKAQ+iNBRljN+1GpzmXgsvb8I4bY7Ollx8t8P8HLknWBwdl//r3//rsn/+1I/5V5x7VnSmH9vM0NH5lX3opZ/0ZX4OyXLb/xa1dbPJ6BvuD/ZZvWzkbhx3JdzreOV2jvFxGBilICMjM/k/B+Vy4AL6VHa5Cqmmcp5PSdcXLMxNNbnTa2B+1/Aog8nZPh0PVqsPr9Z+ENlM0NsTW1bTDut5ZcCoQ3ziHfDRVGUJVEzN0EdDBSn8TpTykADvTOveDj7CGcdqmpviwcowYWOwoZpIBrewOojVOZnmw8ANv3yav9QDOzTvYuccRsNcgdph1vTRDPve7COtOvzv9iWpJ1o5hgPPh81h7HPWf1QlUkz/PBpwJMBC51LtQRTt3oqrA9rt4gL35Q4Bs4Ay0K0jDgV5gqOi/3k6osE0IVBaQhngQlAIaUU4ArTpQbtuTB6rmbkjEOW40sGJRlucFXW0AAAA==","u":""}]} \ No newline at end of file diff --git a/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt b/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt index 10276dfb..25dfb118 100644 --- a/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt @@ -6,7 +6,6 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -39,21 +38,7 @@ object NetworkModule { @Provides @Singleton - @Auth - fun provideAuthOkHttpClient( - loggingInterceptor: HttpLoggingInterceptor, - authInterceptor: Interceptor, - ): OkHttpClient = - OkHttpClient.Builder() - .retryOnConnectionFailure(true) - .addInterceptor(loggingInterceptor) - .addInterceptor(authInterceptor) - .build() - - @Provides - @Singleton - @NoneAuth - fun provideNoneAuthOkHttpClient( + fun provideHttpClient( loggingInterceptor: HttpLoggingInterceptor, ): OkHttpClient = OkHttpClient.Builder() @@ -66,18 +51,8 @@ object NetworkModule { @Provides @Singleton - @Auth - fun provideAuthRetrofit(@Auth okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider): Retrofit = Retrofit.Builder() - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .baseUrl(buildConfigFieldProvider.get().baseUrl) - .client(okHttpClient) - .build() - - @Provides - @Singleton - @NoneAuth - fun provideNoneAuthRetrofit( - @NoneAuth okHttpClient: OkHttpClient, + fun provideRetrofit( + okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider, json: Json, ): Retrofit = @@ -86,14 +61,4 @@ object NetworkModule { .baseUrl(buildConfigFieldProvider.get().baseUrl) .client(okHttpClient) .build() - - @Provides - @Singleton - @S3 - fun provideS3Retrofit(@NoneAuth okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider): Retrofit = - Retrofit.Builder() - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .baseUrl(buildConfigFieldProvider.get().baseUrl) - .client(okHttpClient) - .build() } diff --git a/core/network/src/main/java/com/yapp/network/di/Qualifier.kt b/core/network/src/main/java/com/yapp/network/di/Qualifier.kt deleted file mode 100644 index 68817100..00000000 --- a/core/network/src/main/java/com/yapp/network/di/Qualifier.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.yapp.network.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class NoneAuth - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class Auth - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class S3 diff --git a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt index eb9fb350..8ff592d2 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt @@ -5,8 +5,6 @@ import com.yapp.domain.model.Alarm import kotlinx.coroutines.flow.Flow interface AlarmLocalDataSource { - val firstDismissedAlarmIdFlow: Flow - fun getAllAlarms(): Flow> fun getAlarmsByTime(hour: Int, minute: Int): Flow> suspend fun insertAlarm(alarm: AlarmEntity): Long @@ -14,6 +12,4 @@ interface AlarmLocalDataSource { suspend fun updateAlarmActive(id: Long, active: Boolean): Int suspend fun getAlarm(id: Long): Alarm? suspend fun deleteAlarm(id: Long): Int - suspend fun saveFirstDismissedAlarmId(alarmId: Long) - suspend fun clearDismissedAlarmId() } diff --git a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt index 7c7425b2..03fecd55 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt @@ -3,7 +3,6 @@ package com.yapp.data.local.datasource import com.yapp.database.AlarmDao import com.yapp.database.AlarmEntity import com.yapp.database.toDomain -import com.yapp.datastore.UserPreferences import com.yapp.domain.model.Alarm import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -11,10 +10,7 @@ import javax.inject.Inject class AlarmLocalDataSourceImpl @Inject constructor( private val alarmDao: AlarmDao, - private val userPreferences: UserPreferences, ) : AlarmLocalDataSource { - override val firstDismissedAlarmIdFlow: Flow = userPreferences.firstDismissedAlarmIdFlow - override fun getAllAlarms(): Flow> { return alarmDao.getAllAlarms() .map { alarmEntities -> alarmEntities.map { it.toDomain() } } @@ -45,12 +41,4 @@ class AlarmLocalDataSourceImpl @Inject constructor( override suspend fun deleteAlarm(id: Long): Int { return alarmDao.deleteAlarm(id) } - - override suspend fun saveFirstDismissedAlarmId(alarmId: Long) { - userPreferences.saveFirstDismissedAlarmId(alarmId) - } - - override suspend fun clearDismissedAlarmId() { - userPreferences.clearDismissedAlarmId() - } } diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt index 149234f7..7b574bc0 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt @@ -7,14 +7,21 @@ interface FortuneLocalDataSource { val fortuneDateFlow: Flow val fortuneImageIdFlow: Flow val fortuneScoreFlow: Flow - val hasNewFortuneFlow: Flow - val firstDismissedAlarmIdFlow: Flow + val hasUnseenFortuneFlow: Flow + val shouldShowFortuneToolTipFlow: Flow + val isFortuneCreatingFlow: Flow + val isFortuneFailedFlow: Flow + val isFirstAlarmDismissedTodayFlow: Flow - suspend fun saveFortuneId(fortuneId: Long) - suspend fun markFortuneAsChecked() + suspend fun tryMarkFortuneCreating(): Boolean + suspend fun markFortuneCreating() + suspend fun markFortuneCreated(fortuneId: Long) + suspend fun markFortuneFailed() + suspend fun markFortuneSeen() + suspend fun markFortuneTooltipShown() suspend fun saveFortuneImageId(imageResId: Int) suspend fun saveFortuneScore(score: Int) - suspend fun saveFirstDismissedAlarmId(alarmId: Long) - suspend fun clearDismissedAlarmId() - suspend fun clearFortuneId() + suspend fun markFirstAlarmDismissedToday() + + suspend fun clearFortuneData() } diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt index 6dbe10f9..80712f16 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt @@ -11,15 +11,34 @@ class FortuneLocalDataSourceImpl @Inject constructor( override val fortuneDateFlow = userPreferences.fortuneDateFlow override val fortuneImageIdFlow = userPreferences.fortuneImageIdFlow override val fortuneScoreFlow = userPreferences.fortuneScoreFlow - override val hasNewFortuneFlow = userPreferences.hasNewFortuneFlow - override val firstDismissedAlarmIdFlow = userPreferences.firstDismissedAlarmIdFlow + 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 suspend fun markFortuneCreating() { + userPreferences.markFortuneCreating() + } + + override suspend fun markFortuneCreated(fortuneId: Long) { + userPreferences.markFortuneCreated(fortuneId) + } - override suspend fun saveFortuneId(fortuneId: Long) { - userPreferences.saveFortuneId(fortuneId) + override suspend fun markFortuneFailed() { + userPreferences.markFortuneFailed() } - override suspend fun markFortuneAsChecked() { - userPreferences.markFortuneAsChecked() + override suspend fun markFortuneSeen() { + userPreferences.markFortuneSeen() + } + + override suspend fun markFortuneTooltipShown() { + userPreferences.markFortuneTooltipShown() } override suspend fun saveFortuneImageId(imageResId: Int) { @@ -30,15 +49,11 @@ class FortuneLocalDataSourceImpl @Inject constructor( userPreferences.saveFortuneScore(score) } - override suspend fun saveFirstDismissedAlarmId(alarmId: Long) { - userPreferences.saveFirstDismissedAlarmId(alarmId) - } - - override suspend fun clearDismissedAlarmId() { - userPreferences.clearDismissedAlarmId() + override suspend fun markFirstAlarmDismissedToday() { + userPreferences.markFirstAlarmDismissedToday() } - override suspend fun clearFortuneId() { - userPreferences.clearFortuneId() + override suspend fun clearFortuneData() { + userPreferences.clearFortuneData() } } diff --git a/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt b/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt index 458db14b..be0e97f5 100644 --- a/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt +++ b/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt @@ -1,7 +1,6 @@ package com.yapp.data.remote.di import com.yapp.data.remote.service.ApiService -import com.yapp.network.di.NoneAuth import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,6 +13,6 @@ import javax.inject.Singleton object ServiceModule { @Provides @Singleton - fun providesApiService(@NoneAuth retrofit: Retrofit): ApiService = + fun providesApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java) } diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt index a1c72135..50385b4f 100644 --- a/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt @@ -16,8 +16,6 @@ class AlarmRepositoryImpl @Inject constructor( private val ringtoneManagerHelper: RingtoneManagerHelper, private val soundPlayer: SoundPlayer, ) : AlarmRepository { - override val firstDismissedAlarmIdFlow: Flow = alarmLocalDataSource.firstDismissedAlarmIdFlow - override suspend fun getAlarmSounds(): Result> = runCatching { ringtoneManagerHelper.getAlarmSounds().map { (title, uri) -> AlarmSound(title, uri) @@ -93,12 +91,4 @@ class AlarmRepositoryImpl @Inject constructor( throw Exception("No rows deleted") } } - - override suspend fun saveFirstDismissedAlarmId(alarmId: Long) { - alarmLocalDataSource.saveFirstDismissedAlarmId(alarmId) - } - - override suspend fun clearDismissedAlarmId() { - alarmLocalDataSource.clearDismissedAlarmId() - } } diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt index d3abb0e9..23271c5f 100644 --- a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt @@ -12,32 +12,36 @@ class FortuneRepositoryImpl @Inject constructor( private val fortuneLocalDataSource: FortuneLocalDataSource, private val fortuneRemoteDataSource: FortuneDataSource, ) : FortuneRepository { + override val fortuneIdFlow: Flow = fortuneLocalDataSource.fortuneIdFlow override val fortuneDateFlow: Flow = fortuneLocalDataSource.fortuneDateFlow override val fortuneImageIdFlow: Flow = fortuneLocalDataSource.fortuneImageIdFlow override val fortuneScoreFlow: Flow = fortuneLocalDataSource.fortuneScoreFlow - override val hasNewFortuneFlow: Flow = fortuneLocalDataSource.hasNewFortuneFlow - override val firstDismissedAlarmIdFlow: Flow = fortuneLocalDataSource.firstDismissedAlarmIdFlow + override val hasUnseenFortuneFlow: Flow = fortuneLocalDataSource.hasUnseenFortuneFlow + override val shouldShowFortuneToolTipFlow: Flow = fortuneLocalDataSource.shouldShowFortuneToolTipFlow + override val isFortuneCreatingFlow: Flow = fortuneLocalDataSource.isFortuneCreatingFlow + override val isFortuneFailedFlow: Flow = fortuneLocalDataSource.isFortuneFailedFlow + override val isFirstAlarmDismissedTodayFlow: Flow = fortuneLocalDataSource.isFirstAlarmDismissedTodayFlow - override suspend fun saveFortuneId(fortuneId: Long) = fortuneLocalDataSource.saveFortuneId(fortuneId) - override suspend fun markFortuneAsChecked() = fortuneLocalDataSource.markFortuneAsChecked() + override suspend fun tryMarkFortuneCreating() = fortuneLocalDataSource.tryMarkFortuneCreating() + override suspend fun markFortuneAsCreating() = fortuneLocalDataSource.markFortuneCreating() + override suspend fun markFortuneAsCreated(fortuneId: Long) = fortuneLocalDataSource.markFortuneCreated(fortuneId) + override suspend fun markFortuneAsFailed() = fortuneLocalDataSource.markFortuneFailed() + override suspend fun markFortuneSeen() = fortuneLocalDataSource.markFortuneSeen() + override suspend fun markFortuneTooltipShown() = fortuneLocalDataSource.markFortuneTooltipShown() override suspend fun saveFortuneImageId(imageResId: Int) = fortuneLocalDataSource.saveFortuneImageId(imageResId) override suspend fun saveFortuneScore(score: Int) = fortuneLocalDataSource.saveFortuneScore(score) - override suspend fun saveFirstDismissedAlarmId(alarmId: Long) = fortuneLocalDataSource.saveFirstDismissedAlarmId(alarmId) - override suspend fun clearDismissedAlarmId() = fortuneLocalDataSource.clearDismissedAlarmId() - override suspend fun clearFortuneId() = fortuneLocalDataSource.clearFortuneId() + override suspend fun markFirstAlarmDismissedToday() = fortuneLocalDataSource.markFirstAlarmDismissedToday() + + override suspend fun clearFortuneData() = fortuneLocalDataSource.clearFortuneData() override suspend fun postFortune(userId: Long): Result { return fortuneRemoteDataSource.postFortune(userId) - .mapCatching { fortuneResponse -> - fortuneResponse.toDomain() - } + .mapCatching { it.toDomain() } } override suspend fun getFortune(fortuneId: Long): Result { return fortuneRemoteDataSource.getFortune(fortuneId) - .mapCatching { fortuneResponse -> - fortuneResponse.toDomain() - } + .mapCatching { it.toDomain() } } } diff --git a/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt b/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt index beaead69..7f349ed5 100644 --- a/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt +++ b/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt @@ -1,5 +1,7 @@ package com.yapp.domain.model +import java.time.DayOfWeek + enum class AlarmDay(val bitValue: Int) { SUN(0b0000001), // 1 MON(0b0000010), // 2 @@ -11,8 +13,13 @@ enum class AlarmDay(val bitValue: Int) { ; } -fun AlarmDay.toDayOfWeek(): java.time.DayOfWeek { - return java.time.DayOfWeek.of(((this.ordinal + 6) % 7) + 1) +fun AlarmDay.toDayOfWeek(): DayOfWeek { + return DayOfWeek.of(((this.ordinal + 6) % 7) + 1) +} + +fun DayOfWeek.toAlarmDay(): AlarmDay { + val index = (this.value % 7) + return AlarmDay.entries[index] } fun Set.toRepeatDays(): Int { diff --git a/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt b/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt index f7dac361..60123473 100644 --- a/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt @@ -6,8 +6,6 @@ import com.yapp.domain.model.AlarmSound import kotlinx.coroutines.flow.Flow interface AlarmRepository { - val firstDismissedAlarmIdFlow: Flow - suspend fun getAlarmSounds(): Result> fun initializeSoundPlayer(uri: Uri) fun playAlarmSound(volume: Int) @@ -21,6 +19,4 @@ interface AlarmRepository { suspend fun updateAlarmActive(id: Long, active: Boolean): Result suspend fun getAlarm(id: Long): Result suspend fun deleteAlarm(id: Long): Result - suspend fun saveFirstDismissedAlarmId(alarmId: Long) - suspend fun clearDismissedAlarmId() } diff --git a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt index 23598e3c..89cc4a67 100644 --- a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt @@ -8,16 +8,24 @@ interface FortuneRepository { val fortuneDateFlow: Flow val fortuneImageIdFlow: Flow val fortuneScoreFlow: Flow - val hasNewFortuneFlow: Flow - val firstDismissedAlarmIdFlow: Flow + val hasUnseenFortuneFlow: Flow + val shouldShowFortuneToolTipFlow: Flow + val isFortuneCreatingFlow: Flow + val isFortuneFailedFlow: Flow + val isFirstAlarmDismissedTodayFlow: Flow - suspend fun saveFortuneId(fortuneId: Long) - suspend fun markFortuneAsChecked() + suspend fun tryMarkFortuneCreating(): Boolean + suspend fun markFortuneAsCreating() + suspend fun markFortuneAsCreated(fortuneId: Long) + suspend fun markFortuneAsFailed() + suspend fun markFortuneSeen() + suspend fun markFortuneTooltipShown() suspend fun saveFortuneImageId(imageResId: Int) suspend fun saveFortuneScore(score: Int) - suspend fun saveFirstDismissedAlarmId(alarmId: Long) - suspend fun clearDismissedAlarmId() - suspend fun clearFortuneId() + suspend fun markFirstAlarmDismissedToday() + + suspend fun clearFortuneData() + suspend fun postFortune(userId: Long): Result suspend fun getFortune(fortuneId: Long): Result } diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt index b4ca2c5c..49182b15 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt @@ -7,10 +7,8 @@ import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozeIntent import com.yapp.domain.model.Alarm import com.yapp.domain.model.MissionType -import com.yapp.domain.repository.FortuneRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.firstOrNull import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent @@ -19,7 +17,6 @@ import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate import java.time.LocalTime -import java.time.format.DateTimeFormatter import java.time.format.TextStyle import java.util.Locale import javax.inject.Inject @@ -27,7 +24,6 @@ import javax.inject.Inject @HiltViewModel class AlarmActionViewModel @Inject constructor( private val app: Application, - private val fortuneRepository: FortuneRepository, savedStateHandle: SavedStateHandle, ) : ViewModel(), ContainerHost { @@ -60,15 +56,8 @@ class AlarmActionViewModel @Inject constructor( } private fun fetchShouldShowMissionStart() = intent { - val missionType = alarm?.missionType - val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - val shouldShowMissionStart = missionType != null && - missionType != MissionType.NONE && - fortuneDate != todayDate - reduce { - state.copy(shouldShowMissionStart = shouldShowMissionStart) + state.copy(shouldShowMissionStart = (alarm?.missionType ?: MissionType.NONE) != MissionType.NONE) } } diff --git a/feature/fortune/build.gradle.kts b/feature/fortune/build.gradle.kts index ae450155..543510e8 100644 --- a/feature/fortune/build.gradle.kts +++ b/feature/fortune/build.gradle.kts @@ -12,10 +12,14 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.common) implementation(projects.core.analytics) + implementation(projects.core.alarm) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) implementation(libs.coil.compose) + implementation(libs.androidx.work.runtime) + testImplementation(libs.androidx.work.testing) + androidTestImplementation(libs.androidx.work.testing) implementation(projects.domain) implementation(projects.core.media) } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt index 5f1b5133..0a7b62ce 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import androidx.navigation.navDeepLink import androidx.navigation.navOptions import androidx.navigation.navigation import com.yapp.common.navigation.OrbitNavigator @@ -19,7 +20,11 @@ fun NavGraphBuilder.fortuneNavGraph( snackBarHostState: SnackbarHostState, ) { navigation(startDestination = FortuneDestination.Fortune) { - composable { backStackEntry -> + composable( + deepLinks = listOf( + navDeepLink { uriPattern = "orbitapp://fortune" }, + ), + ) { backStackEntry -> val viewModel = backStackEntry.sharedHiltViewModel(navigator.navController) val coroutineScope = rememberCoroutineScope() diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt index f5109a3b..1eb97abc 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt @@ -3,11 +3,13 @@ package com.yapp.fortune import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable @@ -15,6 +17,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -34,6 +37,7 @@ import com.yapp.fortune.component.FortuneTopAppBar import com.yapp.fortune.component.SlidingIndicator import com.yapp.fortune.page.FortunePager import com.yapp.ui.component.lottie.LottieAnimation +import kotlinx.coroutines.delay import java.math.BigDecimal import java.math.RoundingMode @@ -184,21 +188,49 @@ fun FortuneScreen( @Composable fun FortuneLoadingScreen() { + var isDelivering by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + while (true) { + delay(2000) + isDelivering = !isDelivering + } + } + Box( modifier = Modifier .fillMaxSize() .background(OrbitTheme.colors.gray_900.copy(alpha = 0.7f)), contentAlignment = Alignment.Center, ) { - LottieAnimation( - modifier = Modifier - .size(70.dp), - resId = core.designsystem.R.raw.star_loading, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + val imageRes = if (isDelivering) { + core.designsystem.R.drawable.ic_fortune_delivering_speech_bubble + } else { + core.designsystem.R.drawable.ic_fortune_waiting_speech_bubble + } + Image( + painter = painterResource(id = imageRes), + contentDescription = null, + ) + + LottieAnimation( + modifier = Modifier + .width(375.dp) + .height(267.dp), + resId = core.designsystem.R.raw.fortune_loading, + ) + } } } @Composable @Preview -fun FortuneRoutePreview() { +private fun FortuneLoadingScreenPreview() { + OrbitTheme { + FortuneLoadingScreen() + } } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt index 034c1590..003aa09d 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt @@ -9,6 +9,7 @@ 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.firstOrNull import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -16,8 +17,6 @@ import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container -import java.time.LocalDate -import java.time.format.DateTimeFormatter import javax.inject.Inject @HiltViewModel @@ -30,7 +29,7 @@ class FortuneViewModel @Inject constructor( override val container: Container = container( initialState = FortuneContract.State(), ) { - loadFortune() + observeFortune() } fun processAction(action: FortuneContract.Action) { @@ -50,14 +49,32 @@ class FortuneViewModel @Inject constructor( } } - private fun loadFortune() = intent { - val fortuneId = fortuneRepository.fortuneIdFlow.firstOrNull() - val firstDismissedAlarmId = fortuneRepository.firstDismissedAlarmIdFlow.firstOrNull() - val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() - fortuneId?.let { fetchAndUpdateFortune(it, firstDismissedAlarmId, fortuneDate) } + 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 -> { + reduce { state.copy(isLoading = true) } + } + fortuneId != null -> { + fetchAndUpdateFortune(fortuneId, isFirstAlarmDismissedToday) + } + else -> { + reduce { state.copy(isLoading = false) } + } + } + } } - private fun fetchAndUpdateFortune(fortuneId: Long, firstDismissedAlarmId: Long?, fortuneDate: String?) = intent { + private fun fetchAndUpdateFortune( + fortuneId: Long, + isFirstAlarmDismissedToday: Boolean, + ) = intent { reduce { state.copy(isLoading = true) } fortuneRepository.getFortune(fortuneId).onSuccess { fortune -> @@ -65,8 +82,9 @@ class FortuneViewModel @Inject constructor( val imageId = savedImageId ?: getRandomImage() val formattedTitle = fortune.dailyFortuneTitle.replace(",", ",\n").trim() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - val hasReward = (fortuneDate == todayDate) && (firstDismissedAlarmId != null) + + fortuneRepository.markFortuneSeen() + reduce { state.copy( isLoading = false, @@ -75,7 +93,7 @@ class FortuneViewModel @Inject constructor( avgFortuneScore = fortune.avgFortuneScore, fortunePages = fortune.toFortunePages(), fortuneImageId = imageId, - hasReward = hasReward, + hasReward = isFirstAlarmDismissedToday, ) } }.onFailure { error -> diff --git a/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt b/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt new file mode 100644 index 00000000..47fbd0b0 --- /dev/null +++ b/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt @@ -0,0 +1,19 @@ +package com.yapp.fortune.di + +import com.yapp.alarm.scheduler.PostFortuneTaskScheduler +import com.yapp.fortune.scheduler.WorkManagerPostFortuneTaskScheduler +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SchedulerModule { + @Binds + @Singleton + abstract fun bindsPostFortuneTaskScheduler( + postFortuneTaskScheduler: WorkManagerPostFortuneTaskScheduler, + ): PostFortuneTaskScheduler +} diff --git a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt index 08b7cfc5..aa1dc78b 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt @@ -51,11 +51,13 @@ fun FortunePager( val index = (page - 1).coerceIn(0, state.fortunePages.lastIndex) FortunePageLayout(state.fortunePages[index]) } + 5 -> FortuneCompletePage( hasReward = state.hasReward, onCompleteClick = onNextStep, onNavigateToHome = onNavigateToHome, ) + else -> {} } } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt new file mode 100644 index 00000000..36e49c4f --- /dev/null +++ b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt @@ -0,0 +1,32 @@ +package com.yapp.fortune.scheduler + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.yapp.alarm.scheduler.PostFortuneTaskScheduler +import com.yapp.fortune.worker.PostFortuneWorker +import dagger.hilt.android.qualifiers.ApplicationContext +import java.time.LocalDate +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class WorkManagerPostFortuneTaskScheduler @Inject constructor( + @ApplicationContext private val context: Context, +) : PostFortuneTaskScheduler { + override fun enqueueOnceForToday() { + val name = "post_fortune_${LocalDate.now()}" + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val req = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS) + .build() + WorkManager.getInstance(context) + .enqueueUniqueWork(name, ExistingWorkPolicy.KEEP, req) + } +} diff --git a/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt new file mode 100644 index 00000000..751ae536 --- /dev/null +++ b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt @@ -0,0 +1,54 @@ +package com.yapp.fortune.worker + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.yapp.domain.repository.FortuneRepository +import com.yapp.domain.repository.UserInfoRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull + +@HiltWorker +class PostFortuneWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted params: WorkerParameters, + private val fortuneRepository: FortuneRepository, + private val userInfoRepository: UserInfoRepository, +) : 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() + } + + 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 -> + fortuneRepository.markFortuneAsFailed() + Result.retry() + }, + ) + } catch (_: Throwable) { + fortuneRepository.markFortuneAsFailed() + Result.retry() + } + } +} diff --git a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt index ae347cea..1834e476 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt @@ -338,7 +338,7 @@ class HomeViewModel @Inject constructor( if (fortuneDate != todayDate) { processAction(HomeContract.Action.ShowNoDailyFortuneDialog) } else { - fortuneRepository.markFortuneAsChecked() + fortuneRepository.markFortuneTooltipShown() postSideEffect(HomeContract.SideEffect.NavigateToFortune) } } @@ -349,12 +349,12 @@ class HomeViewModel @Inject constructor( combine( fortuneRepository.fortuneDateFlow, fortuneRepository.fortuneScoreFlow, - fortuneRepository.hasNewFortuneFlow, - ) { fortuneDate, fortuneScore, hasNewFortune -> + fortuneRepository.shouldShowFortuneToolTipFlow, + ) { fortuneDate, fortuneScore, shouldShowTooltip -> val isTodayFortuneAvailable = fortuneDate == todayDate val finalFortuneScore = if (isTodayFortuneAvailable) fortuneScore ?: -1 else -1 - Pair(finalFortuneScore, hasNewFortune) + Pair(finalFortuneScore, shouldShowTooltip) }.collect { (finalFortuneScore, hasNewFortune) -> reduce { state.copy( diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt index 9aa81001..8e5c61d4 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt @@ -27,7 +27,6 @@ sealed class MissionContract { data object ClickCard : Action() data object ShowExitDialog : Action() data object HideExitDialog : Action() - data object RetryPostFortune : Action() } sealed class SideEffect : com.yapp.ui.base.SideEffect { diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt index ad178833..65249b8c 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt @@ -143,12 +143,6 @@ fun MissionScreen( MissionSuccessOverlay() } - state.errorMessage?.let { - ErrorDialog(message = it) { - eventDispatcher(MissionContract.Action.RetryPostFortune) - } - } - if (state.missionMode == MissionMode.PREVIEW) { val insets = WindowInsets.navigationBars.asPaddingValues() @@ -405,16 +399,6 @@ fun MissionSuccessOverlay() { } } -@Composable -fun ErrorDialog(message: String, onConfirm: () -> Unit) { - OrbitDialog( - title = stringResource(id = R.string.error), - message = message, - confirmText = stringResource(id = R.string.confirm), - onConfirm = onConfirm, - ) -} - @Composable fun MissionLoadingScreen() { Box( diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt index 9d8808e4..f54f58e4 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -9,14 +9,11 @@ import com.yapp.analytics.AnalyticsHelper import com.yapp.domain.MissionMode import com.yapp.domain.model.MissionType import com.yapp.domain.repository.FortuneRepository -import com.yapp.domain.repository.UserInfoRepository import com.yapp.media.haptic.HapticFeedbackManager import com.yapp.media.haptic.HapticType import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.first import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent @@ -30,7 +27,6 @@ class MissionViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, private val hapticFeedbackManager: HapticFeedbackManager, private val fortuneRepository: FortuneRepository, - private val userInfoRepository: UserInfoRepository, private val app: Application, private val savedStateHandle: SavedStateHandle, ) : ViewModel(), ContainerHost { @@ -53,7 +49,6 @@ class MissionViewModel @Inject constructor( is MissionContract.Action.ClickCard -> handleMissionProgress(MissionType.TAP) is MissionContract.Action.ShowExitDialog -> showExitDialog() is MissionContract.Action.HideExitDialog -> hideExitDialog() - is MissionContract.Action.RetryPostFortune -> retryPostFortune() } } @@ -138,37 +133,19 @@ class MissionViewModel @Inject constructor( performHapticSuccess() logMissionSuccess(type) if (state.missionMode == MissionMode.REAL) { - postFortune() - } else { - postSideEffect(MissionContract.SideEffect.NavigateBack) - } - } - - private fun postFortune(isRetry: Boolean = false) = intent { - val userId = userInfoRepository.userIdFlow.firstOrNull() ?: return@intent - - val result = withContext(Dispatchers.IO) { - fortuneRepository.postFortune(userId) - } + val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first() + val isFortuneCreating = fortuneRepository.isFortuneCreatingFlow.first() - result.onSuccess { data -> - fortuneRepository.saveFortuneId(data.id) - fortuneRepository.saveFortuneScore(data.avgFortuneScore) - - postSideEffect(MissionContract.SideEffect.NavigateToFortune) - }.onFailure { error -> - if (isRetry) { - navigateToHome() + if (hasUnseenFortune || isFortuneCreating) { + postSideEffect(MissionContract.SideEffect.NavigateToFortune) } else { - reduce { state.copy(errorMessage = error.message) } + postSideEffect(MissionContract.SideEffect.NavigateBack) } + } else { + postSideEffect(MissionContract.SideEffect.NavigateBack) } } - fun retryPostFortune() { - postFortune(isRetry = true) - } - private fun logMissionSuccess(type: String) { analyticsHelper.logEvent( AnalyticsEvent( @@ -183,8 +160,4 @@ class MissionViewModel @Inject constructor( private fun performHapticSuccess() { hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS) } - - private fun navigateToHome() = intent { - postSideEffect(MissionContract.SideEffect.NavigateToHome) - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c10bb87c..db4817c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,7 @@ androidx-app-compat = "1.7.0" androidx-core = "1.15.0" androidx-datastore = "1.1.1" androidx-room = "2.7.2" +androidx-work = "2.10.3" androidx-lifecycle = "2.8.7" @@ -44,6 +45,7 @@ activity-compose = "1.9.3" ## Hilt hilt = "2.51.1" hilt-navigation-compose = "1.2.0" +hilt-work = "1.2.0" ## Third Party okhttp = "4.12.0" @@ -102,6 +104,9 @@ androidx-room-compiler = { group = "androidx.room", name = "room-compiler", vers androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "androidx-room" } androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "androidx-room" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } +androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-work" } +androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidx-work" } +androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work" } ## Compose Libraries activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } @@ -129,6 +134,7 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" } +hilt-worker = { group = "androidx.hilt", name = "hilt-work", version.ref = "hilt-work" } # Orbit orbit-core = { group = "org.orbit-mvi", name = "orbit-core", version.ref = "orbit" }