Initial commit - v1.1.9
This commit is contained in:
350
app/src/main/java/com/example/shiftalarm/AlarmUtils.kt
Normal file
350
app/src/main/java/com/example/shiftalarm/AlarmUtils.kt
Normal file
@@ -0,0 +1,350 @@
|
||||
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)
|
||||
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)
|
||||
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, "===== 전체 알람 동기화 완료 =====")
|
||||
}
|
||||
Reference in New Issue
Block a user