diff --git a/app/src/main/java/com/hotdeal/alarm/worker/AlarmScheduler.kt b/app/src/main/java/com/hotdeal/alarm/worker/AlarmScheduler.kt new file mode 100644 index 0000000..2d854bd --- /dev/null +++ b/app/src/main/java/com/hotdeal/alarm/worker/AlarmScheduler.kt @@ -0,0 +1,116 @@ +package com.hotdeal.alarm.worker + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import com.hotdeal.alarm.receiver.PollingAlarmReceiver +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 실시간 백그라운드 폴링을 위한 AlarmManager 스케줄러 + * + * WorkManager PeriodicWork의 15분 주기 제한 및 Doze 모드 지연을 극복하기 위해 + * AlarmManager (setExactAndAllowWhileIdle / setAndAllowWhileIdle)를 사용하여 + * 사용자가 설정한 주기(1분, 2분, 5분 등)대로 정확하게 백그라운드 수집을 보장합니다. + */ +@Singleton +class AlarmScheduler @Inject constructor( + @ApplicationContext private val context: Context +) { + companion object { + private const val TAG = "AlarmScheduler" + const val ACTION_POLL_ALARM = "com.hotdeal.alarm.ACTION_POLL_ALARM" + private const val REQUEST_CODE = 2001 + } + + private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + /** + * 알람 예약 (Doze mode에서도 정확히 깨어남) + * @param intervalMinutes 폴링 간격 (분) + */ + fun scheduleAlarm(intervalMinutes: Long) { + val safeInterval = intervalMinutes.coerceAtLeast(1L) + val triggerTime = System.currentTimeMillis() + (safeInterval * 60 * 1000L) + + val intent = Intent(context, PollingAlarmReceiver::class.java).apply { + action = ACTION_POLL_ALARM + } + val pendingIntent = PendingIntent.getBroadcast( + context, + REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (alarmManager.canScheduleExactAlarms()) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ) + Log.d(TAG, "Exact Alarm 예약 완료: ${safeInterval}분 후 (${triggerTime})") + } else { + alarmManager.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ) + Log.d(TAG, "Inexact Alarm 예약 완료: ${safeInterval}분 후 (권한 없음)") + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ) + Log.d(TAG, "Exact Alarm 예약 완료 (API < 31): ${safeInterval}분 후") + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ) + } + } catch (e: Exception) { + Log.e(TAG, "Alarm 예약 실패: ${e.message}", e) + try { + alarmManager.setAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ) + } catch (fallbackEx: Exception) { + Log.e(TAG, "Alarm 폴백 예약도 실패: ${fallbackEx.message}") + } + } + } + + /** + * 알람 취소 + */ + fun cancelAlarm() { + val intent = Intent(context, PollingAlarmReceiver::class.java).apply { + action = ACTION_POLL_ALARM + } + val pendingIntent = PendingIntent.getBroadcast( + context, + REQUEST_CODE, + intent, + PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE + ) + + if (pendingIntent != null) { + alarmManager.cancel(pendingIntent) + pendingIntent.cancel() + Log.d(TAG, "Alarm 취소 완료") + } + } +}