Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Timer Service Refactoring #140

Merged
merged 6 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,17 @@
</service>

<service
android:name=".presentation.util.PomodoroTimerService"
android:name=".presentation.service.focus.PomodoroFocusTimerService"
android:foregroundServiceType="specialUse">
<meta-data
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android 14 대응이구나
가이드 보니까 meta-data 말고 property 태그를 사용하던데 차이가 있을까?
https://developer.android.com/about/versions/14/changes/fgs-types-required

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meta-data 가 익숙해서 사용한거였는데, 찾아 보니 property 는 Android 14부터 도입된 개념으로 나오네 이 방법으로 해동 상관 없을거 같아 변경해두고 다시 멘션할게!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android:value="timer_running_in_background" />
</service>

<service
android:name=".presentation.service.rest.PomodoroRestTimerService"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="timer_running_in_background" />
</service>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.pomonyang.mohanyang.presentation.di

import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroTimerRepository
import com.pomonyang.mohanyang.presentation.service.PomodoroTimer
import com.pomonyang.mohanyang.presentation.service.focus.FocusTimer
import com.pomonyang.mohanyang.presentation.service.rest.RestTimer
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent

@Module
@InstallIn(ServiceComponent::class)
internal object PomodoroModule {

@Provides
@FocusTimerType
fun provideFocusTimer(
timerRepository: PomodoroTimerRepository
): PomodoroTimer = FocusTimer(timerRepository = timerRepository)

@Provides
@RestTimerType
fun provideRestTimer(
timerRepository: PomodoroTimerRepository
): PomodoroTimer = RestTimer(timerRepository = timerRepository)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PomodoroNotification

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class FocusTimerType

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RestTimerType
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.pomonyang.mohanyang.presentation.noti

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import androidx.core.app.NotificationCompat
import com.pomonyang.mohanyang.presentation.di.PomodoroNotification
import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.POMODORO_NOTIFICATION_CHANNEL_ID
import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.POMODORO_NOTIFICATION_CHANNEL_NAME
import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.POMODORO_NOTIFICATION_ID
import javax.inject.Inject
import timber.log.Timber

internal class PomodoroNotificationManager @Inject constructor(
@PomodoroNotification private val notificationBuilder: NotificationCompat.Builder,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어제 밤에 이야기 했던 NotificationBuilder 를 hilt를 아직 유지한 이유는 app과 관련된 정보를 presenter에서 알 수가 없다 보니 우선 유지하고 해당 정보들을 어떻게 할지 좀 고민해볼게

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

알림 추가 기능 확인하고 변경해도 괜찮을듯!

private val notificationManager: NotificationManager
) {
fun createNotification(isFocus: Boolean): Notification {
Timber.tag("TIMER").d("createNotification > isFocus $isFocus")
val notificationChannelId = POMODORO_NOTIFICATION_CHANNEL_ID
val notificationChannel = NotificationChannel(
notificationChannelId,
POMODORO_NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(notificationChannel)

return notificationBuilder
.setContentText(if (isFocus) "집중 시간이다냥" else "휴식 시간이다냥")
.build().apply {
flags = Notification.FLAG_NO_CLEAR
}
}

fun notifyFocusEnd() {
notificationManager.notify(
POMODORO_NOTIFICATION_ID,
notificationBuilder.setContentText("집중 시간이 끝났습니다!").build()
)
}

fun notifyFocusExceed() {
notificationManager.notify(
POMODORO_NOTIFICATION_ID,
notificationBuilder.setContentText(
"너무 오랫동안 자리를 비웠다냥"
).build()
)
}

fun notifyRestEnd() {
notificationManager.notify(
POMODORO_NOTIFICATION_ID,
notificationBuilder.setContentText("휴식 시간이 끝났습니다!").build()
)
}

fun notifyRestExceed() {
notificationManager.notify(
POMODORO_NOTIFICATION_ID,
notificationBuilder.setContentText(
"너무 오랫동안 자리를 비웠다냥"
).build()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ import com.pomonyang.mohanyang.presentation.theme.MnTheme
import com.pomonyang.mohanyang.presentation.util.MnNotificationManager
import com.pomonyang.mohanyang.presentation.util.clickableSingle
import com.pomonyang.mohanyang.presentation.util.collectWithLifecycle
import com.pomonyang.mohanyang.presentation.util.stopTimer
import com.pomonyang.mohanyang.presentation.util.stopFocusTimer
import com.pomonyang.mohanyang.presentation.util.stopRestTimer
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -113,8 +114,8 @@ fun PomodoroSettingRoute(
}

private fun stopAllNotification(context: Context) {
context.stopTimer(false)
context.stopTimer(true)
context.stopFocusTimer()
context.stopRestTimer()
MnNotificationManager.stopInterrupt(context)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ import com.pomonyang.mohanyang.presentation.util.DevicePreviews
import com.pomonyang.mohanyang.presentation.util.MnNotificationManager.startInterrupt
import com.pomonyang.mohanyang.presentation.util.MnNotificationManager.stopInterrupt
import com.pomonyang.mohanyang.presentation.util.collectWithLifecycle
import com.pomonyang.mohanyang.presentation.util.startTimer
import com.pomonyang.mohanyang.presentation.util.stopTimer
import com.pomonyang.mohanyang.presentation.util.startFocusTimer
import com.pomonyang.mohanyang.presentation.util.stopFocusTimer

@Composable
fun PomodoroFocusRoute(
Expand Down Expand Up @@ -98,7 +98,7 @@ fun PomodoroFocusRoute(

LaunchedEffect(state.maxFocusTime) {
if (state.maxFocusTime != 0) {
context.startTimer(true, state.maxFocusTime)
context.startFocusTimer(state.maxFocusTime)
}
}

Expand All @@ -125,7 +125,7 @@ fun PomodoroFocusRoute(

private fun stopNotification(context: Context) {
stopInterrupt(context)
context.stopTimer(true)
context.stopFocusTimer()
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import com.pomonyang.mohanyang.presentation.screen.pomodoro.PomodoroTimerViewMod
import com.pomonyang.mohanyang.presentation.theme.MnTheme
import com.pomonyang.mohanyang.presentation.util.DevicePreviews
import com.pomonyang.mohanyang.presentation.util.collectWithLifecycle
import com.pomonyang.mohanyang.presentation.util.startTimer
import com.pomonyang.mohanyang.presentation.util.stopTimer
import com.pomonyang.mohanyang.presentation.util.startRestTimer
import com.pomonyang.mohanyang.presentation.util.stopRestTimer

@Composable
fun PomodoroRestRoute(
Expand All @@ -59,12 +59,12 @@ fun PomodoroRestRoute(
when (effect) {
is PomodoroRestEffect.ShowSnackbar -> onShowSnackbar(effect.message, effect.iconRes)
PomodoroRestEffect.GoToHome -> {
context.stopTimer(false)
context.stopRestTimer()
goToHome()
}

PomodoroRestEffect.GoToPomodoroFocus -> {
context.stopTimer(false)
context.stopRestTimer()
goToFocus()
}
}
Expand All @@ -76,7 +76,7 @@ fun PomodoroRestRoute(

LaunchedEffect(timerState.maxRestTime) {
if (timerState.maxRestTime != 0) {
context.startTimer(false, timerState.maxRestTime)
context.startRestTimer(timerState.maxRestTime)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.pomonyang.mohanyang.presentation.service

internal interface PomodoroTimer {
fun startTimer(maxTime: Int, eventHandler: PomodoroTimerEventHandler)
fun stopTimer()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.pomonyang.mohanyang.presentation.service

internal interface PomodoroTimerEventHandler {
fun onTimeEnd()
fun onTimeExceeded()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.pomonyang.mohanyang.presentation.util
package com.pomonyang.mohanyang.presentation.service

object PomodoroTimerServiceExtras {
const val INTENT_TIMER_IS_FOCUS = "mohanyang.intent.TIMER_FOCUS"
internal object PomodoroTimerServiceExtras {
const val INTENT_TIMER_MAX_TIME = "mohanyang.intent.MAX_TIME"
const val ACTION_TIMER_START = "mohanyang.action.TIMER_START"
const val ACTION_TIMER_STOP = "mohanyang.action.TIMER_STOP"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.pomonyang.mohanyang.presentation.service.focus

import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroTimerRepository
import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.MAX_EXCEEDED_TIME
import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.ONE_SECOND
import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.TIMER_DELAY
import com.pomonyang.mohanyang.presentation.service.PomodoroTimer
import com.pomonyang.mohanyang.presentation.service.PomodoroTimerEventHandler
import java.util.Timer
import javax.inject.Inject
import kotlin.concurrent.fixedRateTimer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber

internal class FocusTimer @Inject constructor(
private val timerRepository: PomodoroTimerRepository
) : PomodoroTimer {

private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var timer: Timer? = null
private var timeElapsed = 0

override fun startTimer(maxTime: Int, eventHandler: PomodoroTimerEventHandler) {
Timber.tag("TIMER").d("startFocus timer / maxTime : $maxTime")
if (timer == null) {
timeElapsed = 0
timer = fixedRateTimer(initialDelay = TIMER_DELAY, period = TIMER_DELAY) {
scope.launch {
timeElapsed += ONE_SECOND
timerRepository.incrementFocusedTime()

Timber.tag("TIMER").d("countFocusTime: $timeElapsed ")

if (timeElapsed >= maxTime) {
eventHandler.onTimeEnd()
} else if (timeElapsed >= maxTime + MAX_EXCEEDED_TIME) {
eventHandler.onTimeExceeded()
stopTimer()
}
}
}
}
}

override fun stopTimer() {
timer?.cancel()
timer = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.pomonyang.mohanyang.presentation.service.focus

import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.pomonyang.mohanyang.presentation.di.FocusTimerType
import com.pomonyang.mohanyang.presentation.noti.PomodoroNotificationManager
import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.POMODORO_NOTIFICATION_ID
import com.pomonyang.mohanyang.presentation.service.PomodoroTimer
import com.pomonyang.mohanyang.presentation.service.PomodoroTimerEventHandler
import com.pomonyang.mohanyang.presentation.service.PomodoroTimerServiceExtras
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import timber.log.Timber

@AndroidEntryPoint
internal class PomodoroFocusTimerService :
Service(),
PomodoroTimerEventHandler {

@FocusTimerType
@Inject
lateinit var focusTimer: PomodoroTimer

@Inject
lateinit var pomodoroNotificationManager: PomodoroNotificationManager

override fun onBind(intent: Intent?): IBinder? = null

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val maxTime = intent.getIntExtra(PomodoroTimerServiceExtras.INTENT_TIMER_MAX_TIME, 0)
Timber.tag("TIMER").d("onStartCommand > ${intent.action} / maxTime: $maxTime")
when (intent.action) {
PomodoroTimerServiceExtras.ACTION_TIMER_START -> {
startForeground(
POMODORO_NOTIFICATION_ID,
pomodoroNotificationManager.createNotification(true)
)
focusTimer.startTimer(maxTime, this)
}

PomodoroTimerServiceExtras.ACTION_TIMER_STOP -> {
stopForeground(STOP_FOREGROUND_REMOVE)
focusTimer.stopTimer()
}
}

return START_NOT_STICKY
}

override fun onTimeEnd() {
pomodoroNotificationManager.notifyFocusEnd()
}

override fun onTimeExceeded() {
pomodoroNotificationManager.notifyFocusExceed()
}

override fun stopService(name: Intent?): Boolean {
stopForeground(STOP_FOREGROUND_REMOVE)
focusTimer.stopTimer()
return super.stopService(name)
}
}
Loading
Loading