Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9d6b45a
[REFACTOR/#246] Fortune 관련 DataStore 상태 및 로직 리팩토링
DongChyeon Aug 20, 2025
16f5dd7
[FEAT/#246] 오늘의 운세 발급 WorkManager 구현
DongChyeon Aug 20, 2025
290f07a
[FEAT/#246] 알람이 울릴 때 FortuneTaskScheduler를 이용해서 오늘의 운세 발급 작업 예약
DongChyeon Aug 20, 2025
35c402c
[FEAT/#246] Hilt 관련 의존성 추가
DongChyeon Aug 20, 2025
f57e976
[REMOVE/#246] 운세 확인 여부와 관계 없이 미션 화면으로 이동하도록 변경
DongChyeon Aug 20, 2025
1240faf
[FEAT/#246] 오늘 울리는 첫 알람 해제 시에만 운세 노출
DongChyeon Aug 20, 2025
9065182
[FEAT/#246] 알람의 미션 타입이 NONE이 아니라면 미션 화면으로 이동
DongChyeon Aug 20, 2025
38ca81e
[FEAT/#246] 이미 생성 중이거나 확인하지 않은 운세가 있을 경우에만 운세 화면으로 이동
DongChyeon Aug 20, 2025
955c93c
[REFACTOR/#246] 운세 데이터를 Flow로 관찰하도록 변경
DongChyeon Aug 20, 2025
1957cbc
[REFACTOR/#246] `hasNewFortuneFlow`를 `shouldShowFortuneToolTipFlow`로 변경
DongChyeon Aug 20, 2025
ac3376d
[FEAT/#246] 운세 확인 안했을 때 알람 클릭 시 운세 화면으로 이동
DongChyeon Aug 20, 2025
ed02958
[FIX/#246] 오늘 운세 생성을 중복 요청하지 않도록 수정
DongChyeon Aug 20, 2025
b277849
[FEAT/#246] 알람 해제 시 isFirstToday 플래그를 Fortune 화면으로 전달하여 보상 여부 판단
DongChyeon Aug 20, 2025
979670c
[REMOVE/#246] `hasTodayFortuneFlow` 삭제
DongChyeon Aug 20, 2025
d30beba
[FIX/#246] compileSdk를 android 블록으로 이동
DongChyeon Aug 20, 2025
9ba43ed
[CHORE/#246] WorkManager 기본 초기화 비활성화
DongChyeon Aug 20, 2025
8f482e5
[REFACTOR/#246] `AlarmInteractionActivityReceiver`에서 `goAsync`를 사용하여 …
DongChyeon Aug 20, 2025
486302d
[CHORE/#246] WorkManager 테스트 의존성 분리
DongChyeon Aug 20, 2025
e155d60
[FIX/#246] `Alarm.ringsToday`로직 수정
DongChyeon Aug 20, 2025
53dff08
[FIX/#246] DataStore에서 데이터를 읽어오지 못할 때 emptyPreferences()를 반환하도록 수정
DongChyeon Aug 20, 2025
ff346c1
[FIX/#246] 단발 알람을 해제했을 때도 비교군에 포함되도록 수정
DongChyeon Aug 20, 2025
abaa8ea
[FIX/#246] 알람 미션이 없을 때에도 shouldShowMissionStart가 true로 설정되는 버그 수정
DongChyeon Aug 20, 2025
9cdb8ba
[REFACTOR/#246] 최초 알람 해제 시 운세 획득 여부를 `SavedStateHandle` 대신 `UserPrefe…
DongChyeon Aug 20, 2025
4b6993a
[REMOVE/#246] UserPreferences, MissionViewModel 디버그 로그 제거
DongChyeon Aug 20, 2025
0cdb890
[FIX/#246] 오늘의 운세가 존재하고, 툴팁이 보여지지 않았을 경우에만 툴팁을 노출하도록 조건 수정
DongChyeon Aug 20, 2025
fdd4fe2
[FIX/#246] 알람 해제 이벤트 로깅 시 earliestIdToday 사용
DongChyeon Aug 21, 2025
3532b26
[FEAT/#246] 운세 생성 로딩 화면 로티 애니메이션 적용
DongChyeon Sep 6, 2025
500862c
[REMOVE/#246] 사용하지 않는 `@Auth`, `@NoneAuth`, `@S3` Qualifier 제거
DongChyeon Sep 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {

android {
namespace = "com.yapp.orbit"
compileSdk = 35

defaultConfig {
versionCode = 6
Expand Down Expand Up @@ -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)
}
14 changes: 11 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="orbitapp"
android:host="mission"/>
<data android:scheme="orbitapp" />
</intent-filter>
</activity>

Expand Down Expand Up @@ -80,5 +78,15 @@
<service
android:name="com.yapp.alarm.services.AlarmService"
android:foregroundServiceType="mediaPlayback" />

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
tools:node="remove" />
</provider>
</application>
</manifest>
13 changes: 12 additions & 1 deletion app/src/main/java/com/yapp/orbit/OrbitApplication.kt
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 2 additions & 0 deletions build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down
3 changes: 0 additions & 3 deletions build-logic/src/main/java/orbit.android.feature.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import com.yapp.convention.configureHiltAndroid
import com.yapp.convention.libs

plugins {
id("orbit.android.library")
id("orbit.android.compose")
}

configureHiltAndroid()

dependencies {
implementation(project(":core:designsystem"))
implementation(project(":core:ui"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
}
Expand Down
47 changes: 31 additions & 16 deletions core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ 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
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

Expand Down Expand Up @@ -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",
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.yapp.alarm.scheduler

interface PostFortuneTaskScheduler {
fun enqueueOnceForToday()
}
19 changes: 7 additions & 12 deletions core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,18 @@ 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
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
Expand All @@ -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())

Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading