diff --git a/app/src/main/java/com/pomonyang/mohanyang/di/ServiceModule.kt b/app/src/main/java/com/pomonyang/mohanyang/di/ServiceModule.kt index 697f892d..c77d9fa1 100644 --- a/app/src/main/java/com/pomonyang/mohanyang/di/ServiceModule.kt +++ b/app/src/main/java/com/pomonyang/mohanyang/di/ServiceModule.kt @@ -27,6 +27,7 @@ internal object ServiceModule { ): NotificationCompat.Builder = NotificationCompat.Builder(context, POMODORO_NOTIFICATION_CHANNEL_ID) .setContentTitle(context.getString(R.string.app_name)) .setSmallIcon(R.drawable.ic_app_notification) + .setVibrate(null) .setOngoing(true) .setContentIntent(ServiceHelper.clickPendingIntent(context, POMODORO_NOTIFICATION_ID)) diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/di/PomodoroModule.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/di/PomodoroModule.kt index cc6444b3..69bc351a 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/di/PomodoroModule.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/di/PomodoroModule.kt @@ -1,6 +1,5 @@ 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 @@ -15,13 +14,9 @@ internal object PomodoroModule { @Provides @FocusTimerType - fun provideFocusTimer( - timerRepository: PomodoroTimerRepository - ): PomodoroTimer = FocusTimer(timerRepository = timerRepository) + fun provideFocusTimer(): PomodoroTimer = FocusTimer() @Provides @RestTimerType - fun provideRestTimer( - timerRepository: PomodoroTimerRepository - ): PomodoroTimer = RestTimer(timerRepository = timerRepository) + fun provideRestTimer(): PomodoroTimer = RestTimer() } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationBitmapGenerator.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationBitmapGenerator.kt new file mode 100644 index 00000000..c11be431 --- /dev/null +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationBitmapGenerator.kt @@ -0,0 +1,168 @@ +package com.pomonyang.mohanyang.presentation.noti + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Typeface +import androidx.annotation.ColorRes +import androidx.annotation.FontRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import com.mohanyang.presentation.R +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal class PomodoroNotificationBitmapGenerator @Inject constructor( + @ApplicationContext private val context: Context +) { + + private val timeBitmaps: Map by lazy { + generateNumberBitmaps( + fontResId = R.font.pretendard_bold, + textSize = 40f, + colorResId = R.color.notification_pomodoro_time + ) + } + + private val overtimeNumberBitmaps: Map by lazy { + generateNumberBitmaps( + fontResId = R.font.pretendard_semibold, + textSize = 18f, + colorResId = R.color.notification_pomodoro_over_time + ) + } + + private val colonBitmap: Bitmap by lazy { + createTextBitmap( + text = ":", + fontResId = R.font.pretendard_bold, + textSize = 40f, + colorResId = R.color.notification_pomodoro_time + ) + } + + private val overtimeColonBitmap: Bitmap by lazy { + createTextBitmap( + text = ":", + fontResId = R.font.pretendard_semibold, + textSize = 18f, + colorResId = R.color.notification_pomodoro_over_time + ) + } + + fun createStatusBitmap(statusText: String): Bitmap = createTextBitmap( + text = statusText, + fontResId = R.font.pretendard_semibold, + textSize = 16f, + colorResId = R.color.notification_pomodoro_category_text + ) + + fun combineTimeBitmaps(time: String, isOvertime: Boolean): Bitmap { + val digits = time.toCharArray() + return combineBitmaps( + digits = digits, + isOvertime = isOvertime + ) + } + + fun createTextBitmap( + @StringRes text: Int, + @ColorRes color: Int, + @FontRes font: Int, + textSize: Float + ): Bitmap = createTextBitmap( + text = context.getString(text), + fontResId = font, + textSize = textSize, + colorResId = color + ) + + private fun generateNumberBitmaps(@FontRes fontResId: Int, textSize: Float, @ColorRes colorResId: Int): Map = (0..9).associate { number -> + number.toString() to createTextBitmap( + text = number.toString(), + fontResId = fontResId, + textSize = textSize, + colorResId = colorResId + ) + } + + private fun createTextBitmap(text: String, @FontRes fontResId: Int, textSize: Float, @ColorRes colorResId: Int): Bitmap { + val typeface = ResourcesCompat.getFont(context, fontResId)!! + val textColor = ContextCompat.getColor(context, colorResId) + return textToBitmap(text, typeface, textSize, textColor) + } + + private fun textToBitmap( + text: String, + typeface: Typeface, + textSize: Float, + textColor: Int + ): Bitmap { + val paint = Paint().apply { + this.typeface = typeface + this.textSize = textSize * context.resources.displayMetrics.density + this.color = textColor + isAntiAlias = true + textAlign = Paint.Align.LEFT + } + val baseline = -paint.ascent() + val width = (paint.measureText(text) + 0.5f).toInt() + val height = (baseline + paint.descent() + 0.5f).toInt() + + return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply { + Canvas(this).drawText(text, 0f, baseline, paint) + } + } + + private fun combineBitmaps( + digits: CharArray, + isOvertime: Boolean + ): Bitmap { + val bitmaps = mutableListOf() + var totalWidth = 0 + var maxHeight = 0 + + digits.forEach { digit -> + val bitmap = when (digit) { + ':' -> if (isOvertime) overtimeColonBitmap else colonBitmap + else -> { + val digitStr = digit.toString() + if (isOvertime) overtimeNumberBitmaps[digitStr] else timeBitmaps[digitStr] + } + } + bitmap?.let { + bitmaps.add(it) + totalWidth += it.width + maxHeight = maxOf(maxHeight, it.height) + } + } + + if (isOvertime) { + val extraTextBitmap = createTextBitmap( + text = context.getString(R.string.timer_exceed_time), + fontResId = R.font.pretendard_semibold, + textSize = 18f, + colorResId = R.color.notification_pomodoro_over_time + ) + bitmaps.add(extraTextBitmap) + totalWidth += extraTextBitmap.width + maxHeight = maxOf(maxHeight, extraTextBitmap.height) + } + + return Bitmap.createBitmap( + totalWidth, + maxHeight, + Bitmap.Config.ARGB_8888 + ).apply { + val canvas = Canvas(this) + var currentX = 0f + + bitmaps.forEach { bitmap -> + canvas.drawBitmap(bitmap, currentX, 0f, null) + currentX += bitmap.width + } + } + } +} diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationContentFactory.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationContentFactory.kt new file mode 100644 index 00000000..0508486f --- /dev/null +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationContentFactory.kt @@ -0,0 +1,125 @@ +package com.pomonyang.mohanyang.presentation.noti + +import android.content.Context +import android.graphics.Bitmap +import android.view.View +import android.widget.RemoteViews +import com.mohanyang.presentation.R +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType +import com.pomonyang.mohanyang.presentation.util.formatTime +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal class PomodoroNotificationContentFactory @Inject constructor( + @ApplicationContext private val context: Context, + private val bitmapGenerator: PomodoroNotificationBitmapGenerator +) { + + fun createPomodoroNotificationContent(isRest: Boolean): RemoteViews { + val remoteViews = RemoteViews(context.packageName, R.layout.notification_pomodoro_standard) + + val titleBitmap = generateTitleBitmap(isRest) + val contentBitmap = generateContentBitmap(isRest) + + setTitle(remoteViews, titleBitmap) + setContent(remoteViews, contentBitmap) + + return remoteViews + } + + fun createPomodoroNotificationBigContent( + category: PomodoroCategoryType?, + time: String, + overtime: String? + ): RemoteViews { + val remoteViews = RemoteViews(context.packageName, R.layout.notification_pomodoro_expand) + + val formattedTime = formatTimeString(time) + val formattedOvertime = formatOvertimeString(overtime) + + val (statusBitmap, iconRes) = getStatusBitmapAndIcon(category) + + setStatus(remoteViews, statusBitmap) + setTime(remoteViews, formattedTime) + setOvertime(remoteViews, formattedOvertime) + setIcon(remoteViews, iconRes) + + return remoteViews + } + + private fun generateTitleBitmap(isRest: Boolean): Bitmap { + val titleRes = if (isRest) R.string.notification_rest_title else R.string.notification_focus_title + return bitmapGenerator.createTextBitmap( + text = titleRes, + color = R.color.notification_pomodoro_title, + font = R.font.pretendard_semibold, + textSize = 14f + ) + } + + private fun generateContentBitmap(isRest: Boolean): Bitmap { + val contentRes = if (isRest) R.string.notification_rest_content else R.string.notification_focus_content + return bitmapGenerator.createTextBitmap( + text = contentRes, + color = R.color.notification_pomodoro_content, + font = R.font.pretendard_regular, + textSize = 14f + ) + } + + private fun setTitle(remoteViews: RemoteViews, titleBitmap: Bitmap) { + remoteViews.setImageViewBitmap(R.id.iv_text_title, titleBitmap) + } + + private fun setContent(remoteViews: RemoteViews, contentBitmap: Bitmap) { + remoteViews.setImageViewBitmap(R.id.iv_text_content, contentBitmap) + } + + private fun formatTimeString(timeStr: String): String { + val totalSeconds = timeStr.toIntOrNull() + return totalSeconds?.formatTime() ?: context.getString(R.string.notification_timer_default_time) + } + + private fun formatOvertimeString(overtime: String?): String? = if (!overtime.isNullOrEmpty() && overtime != "0") { + formatTimeString(overtime) + } else { + null + } + + private fun setStatus(remoteViews: RemoteViews, statusBitmap: Bitmap) { + remoteViews.setImageViewBitmap(R.id.text_status, statusBitmap) + } + + private fun setTime(remoteViews: RemoteViews, formattedTime: String) { + val timeBitmap = bitmapGenerator.combineTimeBitmaps( + time = formattedTime, + isOvertime = false + ) + remoteViews.setImageViewBitmap(R.id.text_time, timeBitmap) + } + + private fun setOvertime(remoteViews: RemoteViews, formattedOvertime: String?) { + if (formattedOvertime != null) { + val overtimeBitmap = bitmapGenerator.combineTimeBitmaps( + time = formattedOvertime, + isOvertime = true + ) + remoteViews.setImageViewBitmap(R.id.text_overtime, overtimeBitmap) + remoteViews.setViewVisibility(R.id.text_overtime, View.VISIBLE) + } else { + remoteViews.setViewVisibility(R.id.text_overtime, View.GONE) + } + } + + private fun setIcon(remoteViews: RemoteViews, iconRes: Int) { + remoteViews.setImageViewResource(R.id.icon_category, iconRes) + } + + private fun getStatusBitmapAndIcon(category: PomodoroCategoryType?): Pair { + val statusTextRes = category?.kor ?: R.string.notification_timer_rest + val statusText = context.getString(statusTextRes) + val statusBitmap = bitmapGenerator.createStatusBitmap(statusText) + val iconRes = category?.iconRes ?: R.drawable.ic_rest + return statusBitmap to iconRes + } +} diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationManager.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationManager.kt index a988ed87..a88518d7 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationManager.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/noti/PomodoroNotificationManager.kt @@ -3,64 +3,79 @@ package com.pomonyang.mohanyang.presentation.noti import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.content.Context +import android.widget.RemoteViews import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.mohanyang.presentation.R import com.pomonyang.mohanyang.presentation.di.PomodoroNotification +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType 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 dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -import timber.log.Timber internal class PomodoroNotificationManager @Inject constructor( + @ApplicationContext private val context: Context, @PomodoroNotification private val notificationBuilder: NotificationCompat.Builder, - private val notificationManager: NotificationManager + private val notificationManager: NotificationManager, + private val contentFactory: PomodoroNotificationContentFactory ) { - 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 - } + init { + createNotificationChannel() } - fun notifyFocusEnd() { - notificationManager.notify( - POMODORO_NOTIFICATION_ID, - notificationBuilder.setContentText("집중 시간이 끝났습니다!").build() - ) + private fun createNotificationChannel() { + val channel = NotificationChannel( + POMODORO_NOTIFICATION_CHANNEL_ID, + POMODORO_NOTIFICATION_CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW + ).apply { + setShowBadge(false) + enableLights(false) + enableVibration(false) + vibrationPattern = null + } + notificationManager.createNotificationChannel(channel) } - fun notifyFocusExceed() { - notificationManager.notify( - POMODORO_NOTIFICATION_ID, - notificationBuilder.setContentText( - "너무 오랫동안 자리를 비웠다냥" - ).build() - ) + fun createNotification(category: PomodoroCategoryType? = null): Notification { + val isRest = category == null + val defaultTime = context.getString(R.string.notification_timer_default_time) + val contentView = createContentView(isRest) + val bigContentView = createBigContentView(category, defaultTime, null) + return buildNotification(contentView, bigContentView) } - fun notifyRestEnd() { - notificationManager.notify( - POMODORO_NOTIFICATION_ID, - notificationBuilder.setContentText("휴식 시간이 끝났습니다!").build() - ) + fun updateNotification(category: PomodoroCategoryType?, time: String, overtime: String) { + val isRest = category == null + val contentView = createContentView(isRest) + val bigContentView = createBigContentView(category, time, overtime) + val notification = buildNotification(contentView, bigContentView) + notificationManager.notify(POMODORO_NOTIFICATION_ID, notification) } - fun notifyRestExceed() { - notificationManager.notify( - POMODORO_NOTIFICATION_ID, - notificationBuilder.setContentText( - "너무 오랫동안 자리를 비웠다냥" - ).build() - ) - } + private fun createContentView(isRest: Boolean): RemoteViews = contentFactory.createPomodoroNotificationContent(isRest) + + private fun createBigContentView( + category: PomodoroCategoryType?, + time: String, + overtime: String? + ): RemoteViews = contentFactory.createPomodoroNotificationBigContent( + category = category, + time = time, + overtime = overtime + ) + + private fun buildNotification( + contentView: RemoteViews, + bigContentView: RemoteViews + ): Notification = notificationBuilder + .setCustomContentView(contentView) + .setCustomBigContentView(bigContentView) + .setColor(ContextCompat.getColor(context, R.color.notification_background_color)) + .setColorized(true) + .build() } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/PomodoroConstants.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/PomodoroConstants.kt index 72567f58..9482f63b 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/PomodoroConstants.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/PomodoroConstants.kt @@ -3,7 +3,7 @@ package com.pomonyang.mohanyang.presentation.screen import com.mohanyang.presentation.BuildConfig object PomodoroConstants { - val TIMER_DELAY = if (BuildConfig.DEBUG) 10L else 1_000L + val TIMER_DELAY = if (BuildConfig.DEBUG) 100L else 1_000L val MAX_EXCEEDED_TIME = if (BuildConfig.DEBUG) 60 else 3600 const val MAX_FOCUS_MINUTES = 60 const val MAX_REST_MINUTES = 30 diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/pomodoro/focus/PomodoroFocusScreen.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/pomodoro/focus/PomodoroFocusScreen.kt index 01ab7198..c68935cf 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/pomodoro/focus/PomodoroFocusScreen.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/pomodoro/focus/PomodoroFocusScreen.kt @@ -98,7 +98,10 @@ fun PomodoroFocusRoute( LaunchedEffect(state.maxFocusTime) { if (state.maxFocusTime != 0) { - context.startFocusTimer(state.maxFocusTime) + context.startFocusTimer( + maxTime = state.maxFocusTime, + category = state.categoryType + ) } } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/BasePomodoroTimer.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/BasePomodoroTimer.kt new file mode 100644 index 00000000..109a9bca --- /dev/null +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/BasePomodoroTimer.kt @@ -0,0 +1,50 @@ +package com.pomonyang.mohanyang.presentation.service + +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType +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 java.util.* +import kotlin.concurrent.fixedRateTimer +import timber.log.Timber + +internal abstract class BasePomodoroTimer : PomodoroTimer { + private var timer: Timer? = null + private var timeElapsed = 0 + + protected abstract fun getTagName(): String + + override fun startTimer( + maxTime: Int, + eventHandler: PomodoroTimerEventHandler, + category: PomodoroCategoryType? + ) { + Timber.tag(getTagName()).d("startTimer / maxTime : $maxTime") + if (timer == null) { + timeElapsed = 0 + timer = fixedRateTimer(initialDelay = TIMER_DELAY, period = TIMER_DELAY) { + timeElapsed += ONE_SECOND + + Timber.tag(getTagName()).d("countTime: $timeElapsed ") + + eventHandler.updateTimer( + time = if (timeElapsed >= maxTime) maxTime.toString() else timeElapsed.toString(), + overtime = if (maxTime >= timeElapsed) "0" else (timeElapsed - maxTime).toString(), + category = category + ) + + if (timeElapsed == maxTime) { + eventHandler.onTimeEnd() + } else if (timeElapsed >= maxTime + MAX_EXCEEDED_TIME) { + eventHandler.onTimeExceeded() + stopTimer() + } + } + } + } + + override fun stopTimer() { + timer?.cancel() + timer = null + } +} diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimer.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimer.kt index 8be7eefe..f1106437 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimer.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimer.kt @@ -1,6 +1,14 @@ package com.pomonyang.mohanyang.presentation.service +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType + internal interface PomodoroTimer { - fun startTimer(maxTime: Int, eventHandler: PomodoroTimerEventHandler) + + fun startTimer( + maxTime: Int, + eventHandler: PomodoroTimerEventHandler, + category: PomodoroCategoryType? = null + ) + fun stopTimer() } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerEventHandler.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerEventHandler.kt index e377c883..64b7c907 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerEventHandler.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerEventHandler.kt @@ -1,6 +1,9 @@ package com.pomonyang.mohanyang.presentation.service +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType + internal interface PomodoroTimerEventHandler { fun onTimeEnd() fun onTimeExceeded() + fun updateTimer(time: String, overtime: String, category: PomodoroCategoryType?) } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerServiceExtras.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerServiceExtras.kt index 3f692d57..a14492e5 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerServiceExtras.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerServiceExtras.kt @@ -2,6 +2,7 @@ package com.pomonyang.mohanyang.presentation.service internal object PomodoroTimerServiceExtras { const val INTENT_TIMER_MAX_TIME = "mohanyang.intent.MAX_TIME" + const val INTENT_FOCUS_CATEGORY = "mohanyang.intent.CATEGORY" const val ACTION_TIMER_START = "mohanyang.action.TIMER_START" const val ACTION_TIMER_STOP = "mohanyang.action.TIMER_STOP" } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/FocusTimer.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/FocusTimer.kt index b19480d5..8d37470e 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/FocusTimer.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/FocusTimer.kt @@ -1,52 +1,9 @@ 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 com.pomonyang.mohanyang.presentation.service.BasePomodoroTimer 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 { +internal class FocusTimer @Inject constructor() : BasePomodoroTimer() { - 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 - } + override fun getTagName(): String = "TIMER_FOCUS" } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/PomodoroFocusTimerService.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/PomodoroFocusTimerService.kt index fea420f0..4ca1dd89 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/PomodoroFocusTimerService.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/PomodoroFocusTimerService.kt @@ -3,14 +3,22 @@ package com.pomonyang.mohanyang.presentation.service.focus import android.app.Service import android.content.Intent import android.os.IBinder +import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroTimerRepository import com.pomonyang.mohanyang.presentation.di.FocusTimerType +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType 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 com.pomonyang.mohanyang.presentation.util.MnNotificationManager +import com.pomonyang.mohanyang.presentation.util.getSerializableExtraCompat import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import timber.log.Timber @AndroidEntryPoint @@ -25,18 +33,29 @@ internal class PomodoroFocusTimerService : @Inject lateinit var pomodoroNotificationManager: PomodoroNotificationManager + @Inject + lateinit var timerRepository: PomodoroTimerRepository + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + 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") + val category = intent.getSerializableExtraCompat(PomodoroTimerServiceExtras.INTENT_FOCUS_CATEGORY) + + Timber.tag("TIMER").d("onStartCommand > ${intent.action} / maxTime: $maxTime / category $category") when (intent.action) { PomodoroTimerServiceExtras.ACTION_TIMER_START -> { startForeground( POMODORO_NOTIFICATION_ID, - pomodoroNotificationManager.createNotification(true) + pomodoroNotificationManager.createNotification(category) + ) + focusTimer.startTimer( + maxTime = maxTime, + eventHandler = this, + category = category ) - focusTimer.startTimer(maxTime, this) } PomodoroTimerServiceExtras.ACTION_TIMER_STOP -> { @@ -49,11 +68,23 @@ internal class PomodoroFocusTimerService : } override fun onTimeEnd() { - pomodoroNotificationManager.notifyFocusEnd() + Timber.tag("TIMER").d("onFocusTimeEnd") + MnNotificationManager.notifyFocusEnd(context = this) } override fun onTimeExceeded() { - pomodoroNotificationManager.notifyFocusExceed() + // TODO 여기 뭔가 로직이 필요하면 그때 추가 + } + + override fun updateTimer(time: String, overtime: String, category: PomodoroCategoryType?) { + scope.launch { + timerRepository.incrementFocusedTime() + } + pomodoroNotificationManager.updateNotification( + time = time, + overtime = overtime, + category = category + ) } override fun stopService(name: Intent?): Boolean { diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/PomodoroRestTimerService.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/PomodoroRestTimerService.kt index 9e5ec6d5..0de3f19c 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/PomodoroRestTimerService.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/PomodoroRestTimerService.kt @@ -3,14 +3,22 @@ package com.pomonyang.mohanyang.presentation.service.rest import android.app.Service import android.content.Intent import android.os.IBinder +import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroTimerRepository import com.pomonyang.mohanyang.presentation.di.RestTimerType +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType import com.pomonyang.mohanyang.presentation.noti.PomodoroNotificationManager +import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants import com.pomonyang.mohanyang.presentation.service.PomodoroTimer import com.pomonyang.mohanyang.presentation.service.PomodoroTimerEventHandler import com.pomonyang.mohanyang.presentation.service.PomodoroTimerServiceExtras +import com.pomonyang.mohanyang.presentation.util.MnNotificationManager import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject -import kotlin.random.Random +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import timber.log.Timber @AndroidEntryPoint internal class PomodoroRestTimerService : @@ -24,6 +32,11 @@ internal class PomodoroRestTimerService : @Inject lateinit var pomodoroNotificationManager: PomodoroNotificationManager + @Inject + lateinit var timerRepository: PomodoroTimerRepository + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + override fun onBind(intent: Intent?): IBinder? = null override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { @@ -31,10 +44,13 @@ internal class PomodoroRestTimerService : when (intent.action) { PomodoroTimerServiceExtras.ACTION_TIMER_START -> { startForeground( - Random.nextInt(), - pomodoroNotificationManager.createNotification(false) + PomodoroConstants.POMODORO_NOTIFICATION_ID, + pomodoroNotificationManager.createNotification() + ) + restTimer.startTimer( + maxTime = maxTime, + eventHandler = this ) - restTimer.startTimer(maxTime, this) } PomodoroTimerServiceExtras.ACTION_TIMER_STOP -> { @@ -47,11 +63,23 @@ internal class PomodoroRestTimerService : } override fun onTimeEnd() { - pomodoroNotificationManager.notifyRestEnd() + Timber.tag("TIMER").d("onRestTimeEnd") + MnNotificationManager.notifyRestEnd(context = this) } override fun onTimeExceeded() { - pomodoroNotificationManager.notifyRestExceed() + // TODO 여기 뭔가 로직이 필요하면 그때 추가 + } + + override fun updateTimer(time: String, overtime: String, category: PomodoroCategoryType?) { + scope.launch { + timerRepository.incrementRestedTime() + } + pomodoroNotificationManager.updateNotification( + time = time, + overtime = overtime, + category = category + ) } override fun stopService(name: Intent?): Boolean { diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/RestTimer.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/RestTimer.kt index c8eaf877..754ce944 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/RestTimer.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/RestTimer.kt @@ -1,51 +1,9 @@ package com.pomonyang.mohanyang.presentation.service.rest -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 com.pomonyang.mohanyang.presentation.service.BasePomodoroTimer import javax.inject.Inject -import kotlin.concurrent.fixedRateTimer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import timber.log.Timber -internal class RestTimer @Inject constructor( - private val timerRepository: PomodoroTimerRepository -) : PomodoroTimer { +internal class RestTimer @Inject constructor() : BasePomodoroTimer() { - private var timer: Timer? = null - private var timeElapsed = 0 - private val scope = CoroutineScope(Dispatchers.IO) - - override fun startTimer(maxTime: Int, eventHandler: PomodoroTimerEventHandler) { - Timber.tag("TIMER").d("startRest timer / maxTime : $maxTime") - if (timer == null) { - timeElapsed = 0 - timer = fixedRateTimer(initialDelay = TIMER_DELAY, period = TIMER_DELAY) { - scope.launch { - timeElapsed += ONE_SECOND - timerRepository.incrementRestedTime() - - Timber.tag("TIMER").d("countRestTime: $timeElapsed ") - - if (timeElapsed >= maxTime) { - eventHandler.onTimeEnd() - } else if (timeElapsed >= maxTime + MAX_EXCEEDED_TIME) { - eventHandler.onTimeExceeded() - stopTimer() - } - } - } - } - } - - override fun stopTimer() { - timer?.cancel() - timer = null - } + override fun getTagName(): String = "TIMER_REST" } diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/IntentUtils.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/IntentUtils.kt new file mode 100644 index 00000000..3b16e6a2 --- /dev/null +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/IntentUtils.kt @@ -0,0 +1,12 @@ +package com.pomonyang.mohanyang.presentation.util + +import android.content.Intent +import android.os.Build +import java.io.Serializable + +inline fun Intent.getSerializableExtraCompat(key: String): T? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getSerializableExtra(key, T::class.java) +} else { + @Suppress("DEPRECATION") + getSerializableExtra(key) as? T +} diff --git a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/TimerUtils.kt b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/TimerUtils.kt index 39a34d3d..98142535 100644 --- a/presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/TimerUtils.kt +++ b/presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/TimerUtils.kt @@ -3,19 +3,24 @@ package com.pomonyang.mohanyang.presentation.util import android.content.Context import android.content.Intent import androidx.core.os.bundleOf +import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType import com.pomonyang.mohanyang.presentation.service.PomodoroTimerServiceExtras import com.pomonyang.mohanyang.presentation.service.focus.PomodoroFocusTimerService import com.pomonyang.mohanyang.presentation.service.rest.PomodoroRestTimerService import timber.log.Timber -internal fun Context.startFocusTimer(maxTime: Int) { +internal fun Context.startFocusTimer( + maxTime: Int, + category: PomodoroCategoryType +) { Timber.tag("TIMER").d("startFocusTimer") startService( Intent(this, PomodoroFocusTimerService::class.java).apply { action = PomodoroTimerServiceExtras.ACTION_TIMER_START putExtras( bundleOf( - PomodoroTimerServiceExtras.INTENT_TIMER_MAX_TIME to maxTime + PomodoroTimerServiceExtras.INTENT_TIMER_MAX_TIME to maxTime, + PomodoroTimerServiceExtras.INTENT_FOCUS_CATEGORY to category ) ) } diff --git a/presentation/src/main/res/layout/notification_pomodoro_expand.xml b/presentation/src/main/res/layout/notification_pomodoro_expand.xml new file mode 100644 index 00000000..96a5a570 --- /dev/null +++ b/presentation/src/main/res/layout/notification_pomodoro_expand.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/notification_pomodoro_standard.xml b/presentation/src/main/res/layout/notification_pomodoro_standard.xml new file mode 100644 index 00000000..0f48cac4 --- /dev/null +++ b/presentation/src/main/res/layout/notification_pomodoro_standard.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml new file mode 100644 index 00000000..7f4fb70b --- /dev/null +++ b/presentation/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFF2E6 + #000000 + #8F887E + #3D3732 + #FF7E65 + #1D1B1B + #3F4946 + \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index d58882ed..778dc18a 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -87,6 +87,12 @@ 안드로이드 설정에서 모하냥 앱의 알림 표시를 허용하면 Push 알림을 받을 수 있어요. 지금 설정하시겠어요? 다음에 설정으로 이동 + 00:00 + 휴식중 + 휴식을 취하고 있어요 + 지금은 편안한 쉬는 시간이다냥 + 집중하고 있어요 + 뽀모도로에 푹 빠져있다냥 네트워크 연결 실패 네트워크 연결에 실패하였습니다.\n확인 후 다시 시도해 주세요 새로고침