package com.example.shiftalarm 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 android.widget.Toast import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId import java.util.concurrent.TimeUnit val SEOUL_ZONE: ZoneId = ZoneId.of("Asia/Seoul") const val TAG = "ShiftAlarm" // ============================================ // 알람 ID 생성 // ============================================ fun getCustomAlarmId(date: LocalDate, uniqueId: Int): Int { // Combine date and a unique ID from DB to avoid collisions // Using (uniqueId % 1000) to keep it within a reasonable range return 200000000 + (date.year % 100) * 1000000 + date.monthValue * 10000 + date.dayOfMonth * 100 + (uniqueId % 100) } // ============================================ // 사용자 알람 예약 // ============================================ fun scheduleCustomAlarm( context: Context, date: LocalDate, uniqueId: Int, shiftType: String, time: String, soundUri: String? = null, snoozeMin: Int = 5, snoozeRepeat: Int = 3 ) { val alarmId = getCustomAlarmId(date, uniqueId) val label = "사용자:$shiftType" val parts = time.split(":") if (parts.size != 2) return val hour = parts[0].toIntOrNull() ?: return val min = parts[1].toIntOrNull() ?: return cancelAlarmInternal(context, alarmId) val intent = Intent(context, AlarmReceiver::class.java).apply { action = "com.example.shiftalarm.ALARM_TRIGGER" putExtra("EXTRA_SHIFT", label) putExtra("EXTRA_DATE", date.toString()) putExtra("EXTRA_TIME", time) putExtra("EXTRA_ALARM_ID", alarmId) putExtra("EXTRA_IS_CUSTOM", true) putExtra("EXTRA_UNIQUE_ID", uniqueId) // DB 검증용 putExtra("EXTRA_SOUND", soundUri) putExtra("EXTRA_SNOOZE", snoozeMin) putExtra("EXTRA_SNOOZE_REPEAT", snoozeRepeat) } val pendingIntent = PendingIntent.getBroadcast( context, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val targetDateTime = LocalDateTime.of(date, LocalTime.of(hour, min)) .withSecond(0).withNano(0) val alarmTime = targetDateTime.atZone(SEOUL_ZONE).toInstant().toEpochMilli() if (alarmTime > System.currentTimeMillis()) { setExactAlarm(context, alarmTime, pendingIntent) Log.d(TAG, "알람 예약 완료: $date $time (ID: $alarmId)") } } // ============================================ // 알람 취소 (전체 범위) // ============================================ fun cancelCustomAlarm(context: Context, date: LocalDate, uniqueId: Int) { val alarmId = getCustomAlarmId(date, uniqueId) cancelAlarmInternal(context, alarmId) } /** * 특정 알람의 모든 예약을 완전히 취소합니다. * DB에서 삭제하기 전에 반드시 호출해야 합니다. * 삭제한 알람이 울리는 문제를 해결하기 위해 365일치 + 과거 알람까지 모두 취소 */ fun cancelAllCustomAlarmSchedules(context: Context, uniqueId: Int) { val today = LocalDate.now(SEOUL_ZONE) // 1. 과거 30일치 취소 (혹시 모를 과거 예약) for (i in -30 until 0) { val targetDate = today.plusDays(i.toLong()) cancelCustomAlarm(context, targetDate, uniqueId) } // 2. 향후 365일치 모든 가능한 ID 취소 (1년치 완전 커버) for (i in 0 until 365) { val targetDate = today.plusDays(i.toLong()) cancelCustomAlarm(context, targetDate, uniqueId) } // 3. 스누즈 알람도 취소 (스누즈는 999999 ID 사용) cancelSnoozeAlarm(context) // 4. 테스트 알람도 취소 (테스트는 888888 ID 사용) cancelTestAlarm(context) // 5. 해당 uniqueId와 관련된 모든 가능한 PendingIntent 취소 (추가 안전장치) cancelAllPendingIntentsForUniqueId(context, uniqueId) Log.d(TAG, "알람 예약 완전 취소 완료 (ID: $uniqueId, 범위: -30일 ~ +365일)") } /** * 특정 uniqueId에 대한 모든 가능한 PendingIntent를 취소합니다. * 알람 ID 생성 공식의 역연산을 통해 모든 가능성을 커버합니다. */ private fun cancelAllPendingIntentsForUniqueId(context: Context, uniqueId: Int) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager // uniqueId % 100의 모든 가능한 값에 대해 취소 시도 val baseId = uniqueId % 100 // 현재 연도 기준으로 여러 해에 걸친 가능한 ID들 val currentYear = LocalDate.now(SEOUL_ZONE).year % 100 val years = listOf(currentYear - 1, currentYear, currentYear + 1) for (year in years) { if (year < 0) continue for (month in 1..12) { for (day in 1..31) { try { val alarmId = 200000000 + year * 1000000 + month * 10000 + day * 100 + baseId val intent = Intent(context, AlarmReceiver::class.java).apply { action = "com.example.shiftalarm.ALARM_TRIGGER" } val pendingIntent = PendingIntent.getBroadcast( context, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) alarmManager.cancel(pendingIntent) pendingIntent.cancel() } catch (e: Exception) { // 무시 - 유효하지 않은 날짜 조합 } } } } Log.d(TAG, "uniqueId $uniqueId 관련 모든 PendingIntent 취소 완료") } /** * 스누즈 알람 취소 */ /** * 스누즈 알람 취소 - 모든 가능한 스누즈 ID 취소 */ fun cancelSnoozeAlarm(context: Context) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager // 주요 스누즈 ID들 취소 val snoozeIds = listOf(999999, 999998, 999997, 999996, 999995) for (snoozeId in snoozeIds) { val intent = Intent(context, AlarmReceiver::class.java).apply { action = "com.example.shiftalarm.SNOOZE" } val pendingIntent = PendingIntent.getBroadcast( context, snoozeId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) alarmManager.cancel(pendingIntent) pendingIntent.cancel() } Log.d(TAG, "스누즈 알람 취소 완료") } /** * 테스트 알람 취소 */ private fun cancelTestAlarm(context: Context) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(context, AlarmReceiver::class.java).apply { action = "com.example.shiftalarm.ALARM_TRIGGER" } val pendingIntent = PendingIntent.getBroadcast( context, 888888, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) alarmManager.cancel(pendingIntent) pendingIntent.cancel() Log.d(TAG, "테스트 알람 취소 완료") } private fun cancelAlarmInternal(context: Context, alarmId: Int) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(context, AlarmReceiver::class.java).apply { action = "com.example.shiftalarm.ALARM_TRIGGER" } val pendingIntent = PendingIntent.getBroadcast( context, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) alarmManager.cancel(pendingIntent) pendingIntent.cancel() } // ============================================ // 정밀 알람 설정 (setAlarmClock 우선) // ============================================ private fun setExactAlarm(context: Context, triggerTime: Long, pendingIntent: PendingIntent) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (!alarmManager.canScheduleExactAlarms()) { Log.e(TAG, "정확한 알람 권한 없음!") return } } // setAlarmClock은 Doze 모드에서도 정확하게 작동하며 상단바 알람 아이콘을 활성화함 (신뢰도 최고) try { val viewIntent = Intent(context, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) } val viewPendingIntent = PendingIntent.getActivity( context, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val clockInfo = AlarmManager.AlarmClockInfo(triggerTime, viewPendingIntent) alarmManager.setAlarmClock(clockInfo, pendingIntent) Log.d(TAG, "setAlarmClock 예약 성공: ${java.util.Date(triggerTime)}") } catch (e: Exception) { Log.e(TAG, "setAlarmClock 실패, fallback 사용", e) try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent) Log.d(TAG, "setExactAndAllowWhileIdle 예약 성공") } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent) Log.d(TAG, "setExact 예약 성공") } } catch (e2: Exception) { Log.e(TAG, "모든 알람 예약 방법 실패", e2) } } } // ============================================ // 스누즈 // ============================================ fun scheduleSnooze(context: Context, snoozeMin: Int, soundUri: String? = null, snoozeRepeat: Int = 3) { val intent = Intent(context, AlarmReceiver::class.java).apply { action = "com.example.shiftalarm.SNOOZE" putExtra("EXTRA_SHIFT", "SNOOZE") putExtra("EXTRA_SOUND", soundUri) putExtra("EXTRA_SNOOZE", snoozeMin) putExtra("EXTRA_SNOOZE_REPEAT", snoozeRepeat) } val pendingIntent = PendingIntent.getBroadcast( context, 999999, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val triggerTime = System.currentTimeMillis() + (snoozeMin * 60 * 1000) setExactAlarm(context, triggerTime, pendingIntent) } // ============================================ // 테스트 알람 (5초 후) // ============================================ fun scheduleTestAlarm(context: Context) { val intent = Intent(context, AlarmReceiver::class.java).apply { action = "com.example.shiftalarm.ALARM_TRIGGER" putExtra("EXTRA_SHIFT", "테스트") } val pendingIntent = PendingIntent.getBroadcast( context, 888888, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val triggerTime = System.currentTimeMillis() + 5000 setExactAlarm(context, triggerTime, pendingIntent) } // ============================================ // 전체 동기화 (30일치 예약) // ============================================ suspend fun syncAllAlarms(context: Context) { Log.d(TAG, "===== 전체 알람 동기화 시작 (30일) =====") val prefs = context.getSharedPreferences("ShiftAlarmPrefs", Context.MODE_PRIVATE) val repo = ShiftRepository(context) val today = LocalDate.now(SEOUL_ZONE) val team = prefs.getString("selected_team", "A") ?: "A" val factory = prefs.getString("selected_factory", "Jeonju") ?: "Jeonju" // 1. 기존 알람 모두 취소 (안전장치) // Custom 알람의 경우 ID가 uniqueId 기반이므로 모든 가능성 있는 ID를 취소하기는 어려움. // 대신 AlarmManager에서 해당 PendingIntent를 정확히 취소해야 함. // 하지만 uniqueId를 알 수 없으므로, 모든 날짜 루프에서 취소 시도. val customAlarms = repo.getAllCustomAlarms() for (i in 0 until 30) { val targetDate = today.plusDays(i.toLong()) // 기본 알람 ID 취소 (이제 안 쓰지만 하위 호환/청소용) val legacyId = 100000000 + (targetDate.year % 100) * 1000000 + targetDate.monthValue * 10000 + targetDate.dayOfMonth * 100 cancelAlarmInternal(context, legacyId) // 커스텀 알람 취소 customAlarms.forEach { alarm -> cancelCustomAlarm(context, targetDate, alarm.id) } } if (!ShiftAlarmDefaults.isMasterAlarmEnabled(prefs)) { Log.d(TAG, "마스터 알람이 꺼져 있어 예약을 중단합니다.") return } // 2. 새로운 스케줄 생성 for (i in 0 until 30) { val targetDate = today.plusDays(i.toLong()) val shift = repo.getShift(targetDate, team, factory) for (alarm in customAlarms) { if (!alarm.isEnabled) continue // 근무 연동 조건 확인 if (alarm.shiftType == "기타" || alarm.shiftType == shift) { scheduleCustomAlarm( context, targetDate, alarm.id, alarm.shiftType, alarm.time, alarm.soundUri, alarm.snoozeInterval, alarm.snoozeRepeat ) } } } Log.d(TAG, "===== 전체 알람 동기화 완료 =====") }