Files
ShiftRing/app/src/main/java/com/example/shiftalarm/FragmentSettingsAlarm.kt
T

750 lines
34 KiB
Kotlin

package com.example.shiftalarm
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.TimePicker
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.google.android.material.materialswitch.MaterialSwitch
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.example.shiftalarm.databinding.FragmentSettingsAlarmBinding
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONObject
import java.time.LocalDate
class FragmentSettingsAlarm : Fragment(), SharedPreferences.OnSharedPreferenceChangeListener {
private var _binding: FragmentSettingsAlarmBinding? = null
private val binding get() = _binding!!
private val PREFS_NAME = "ShiftAlarmPrefs"
private lateinit var repository: ShiftRepository
private var customAlarms: MutableList<CustomAlarm> = mutableListOf()
private val collapsedGroups = mutableSetOf<String>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsAlarmBinding.inflate(inflater, container, false)
repository = ShiftRepository(requireContext())
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.registerOnSharedPreferenceChangeListener(this)
setupListeners()
loadSettings()
}
override fun onResume() {
super.onResume()
refreshAlarmList()
}
override fun onDestroyView() {
super.onDestroyView()
val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.unregisterOnSharedPreferenceChangeListener(this)
_binding = null
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == "master_alarm_enabled") {
sharedPreferences?.let {
updateMasterToggleUI(ShiftAlarmDefaults.isMasterAlarmEnabled(it))
}
}
}
private fun loadSettings() {
val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Master Toggle Button State
updateMasterToggleUI(ShiftAlarmDefaults.isMasterAlarmEnabled(prefs))
// Migrate and Refresh
lifecycleScope.launch {
migrateFromPrefsIfNecessary()
refreshAlarmList()
}
}
private suspend fun migrateFromPrefsIfNecessary() {
val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val legacyJson = prefs.getString("custom_alarms", null)
if (legacyJson != null) {
try {
val arr = JSONArray(legacyJson)
for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i)
val alarm = CustomAlarm(
time = obj.getString("time"),
shiftType = obj.getString("shiftType"),
isEnabled = obj.optBoolean("enabled", true),
soundUri = obj.optString("soundUri", null),
snoozeInterval = obj.optInt("snoozeInterval", 5),
snoozeRepeat = obj.optInt("snoozeRepeat", 3)
)
repository.addCustomAlarm(alarm)
}
// Clear legacy data
prefs.edit().remove("custom_alarms").apply()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun refreshAlarmList() {
lifecycleScope.launch {
customAlarms = repository.getAllCustomAlarms().toMutableList()
if (customAlarms.isEmpty()) {
// 주간, 석간, 야간 기본 알람 자동 생성
val defaultAlarms = listOf(
CustomAlarm(time = "07:00", shiftType = "주간", isEnabled = true),
CustomAlarm(time = "15:00", shiftType = "석간", isEnabled = true),
CustomAlarm(time = "23:00", shiftType = "야간", isEnabled = true)
)
for (def in defaultAlarms) {
repository.addCustomAlarm(def)
}
customAlarms = repository.getAllCustomAlarms().toMutableList()
syncAllAlarms(requireContext())
}
refreshUI()
}
}
private val soundTitleCache = mutableMapOf<String?, String>()
private fun updateMasterToggleUI(isEnabled: Boolean) {
if (isEnabled) {
binding.tvMasterStatus.text = "전체 알람 켜짐"
binding.tvMasterStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.primary))
binding.tvMasterStatus.backgroundTintList = android.content.res.ColorStateList.valueOf(Color.parseColor("#E3F2FD"))
} else {
binding.tvMasterStatus.text = "전체 알람 꺼짐"
binding.tvMasterStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.shift_red))
binding.tvMasterStatus.backgroundTintList = android.content.res.ColorStateList.valueOf(Color.parseColor("#FFEBEE"))
}
}
private fun setupListeners() {
val prefs = requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
binding.tvMasterStatus.setOnClickListener {
val isEnabled = !ShiftAlarmDefaults.isMasterAlarmEnabled(prefs)
prefs.edit().putBoolean("master_alarm_enabled", isEnabled).apply()
updateMasterToggleUI(isEnabled)
val message = if (isEnabled) "전체 알람이 켜졌습니다." else "전체 알람이 꺼졌습니다."
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
// Resync immediately
lifecycleScope.launch { syncAllAlarms(requireContext()) }
}
binding.btnAddCustomAlarm.setOnClickListener {
showEditDialog(
title = "새 알람 추가",
currentTime = "07:00",
shiftType = "주간",
existingAlarm = null,
isNew = true
)
}
binding.btnTestAlarm.setOnClickListener {
scheduleTestAlarm(requireContext())
}
}
private fun refreshUI() {
val container = binding.alarmListContainer
container.removeAllViews()
if (customAlarms.isEmpty()) {
val emptyView = TextView(requireContext()).apply {
text = "등록된 알람이 없습니다.\n아래 [새 알람 추가] 버튼을 눌러 추가해보세요."
textSize = 14f
setTextColor(ContextCompat.getColor(context, R.color.text_tertiary))
gravity = android.view.Gravity.CENTER
setPadding(0, dpToPx(40f), 0, dpToPx(40f))
}
container.addView(emptyView)
return
}
// Group alarms by Shift Type: 주간, 석간, 야간, 주간 맞교대, 야간 맞교대, 기타
val groups = linkedMapOf<String, MutableList<CustomAlarm>>()
val displayOrder = listOf("주간", "석간", "야간", "주간 맞교대", "야간 맞교대", "기타")
for (alarm in customAlarms) {
val category = when(alarm.shiftType) {
"주간" -> "주간"
"석간" -> "석간"
"야간" -> "야간"
"주간 맞교대", "주맞" -> "주간 맞교대"
"야간 맞교대", "야맞" -> "야간 맞교대"
else -> "기타"
}
groups.getOrPut(category) { mutableListOf() }.add(alarm)
}
for (category in displayOrder) {
val groupAlarms = groups[category] ?: continue
if (groupAlarms.isEmpty()) continue
// 1. Group Header View
val headerView = layoutInflater.inflate(R.layout.item_alarm_group_header, container, false)
val ivExpand = headerView.findViewById<ImageView>(R.id.ivExpand)
val tvGroupName = headerView.findViewById<TextView>(R.id.tvGroupName)
val tvAlarmCount = headerView.findViewById<TextView>(R.id.tvAlarmCount)
val groupSwitch = headerView.findViewById<MaterialSwitch>(R.id.groupSwitch)
tvGroupName.text = "$category 알람"
tvAlarmCount.text = "${groupAlarms.size}개 알람"
val allEnabled = groupAlarms.all { it.isEnabled }
groupSwitch.isChecked = allEnabled
val isCollapsed = collapsedGroups.contains(category)
ivExpand.rotation = if (isCollapsed) 0f else 45f
// Group Switch Toggle (Master On/Off for this group)
groupSwitch.setOnCheckedChangeListener { _, isChecked ->
lifecycleScope.launch {
for (alarm in groupAlarms) {
if (alarm.isEnabled != isChecked) {
AlarmSyncManager.toggleAlarm(requireContext(), alarm, isChecked)
}
}
refreshAlarmList()
}
}
// Container for child rows
val childrenContainer = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
visibility = if (isCollapsed) View.GONE else View.VISIBLE
}
// Click header to collapse/expand with smooth accordion animation
headerView.setOnClickListener {
val willExpand = collapsedGroups.contains(category)
// 1. Smooth accordion expand/collapse transition
val transition = android.transition.AutoTransition().apply {
duration = 250
interpolator = android.view.animation.AccelerateDecelerateInterpolator()
}
android.transition.TransitionManager.beginDelayedTransition(container, transition)
// 2. Smooth rotation between '+' (0 deg) and 'x' (45 deg)
val targetRotation = if (willExpand) 45f else 0f
ivExpand.animate()
.rotation(targetRotation)
.setDuration(250)
.setInterpolator(androidx.core.view.animation.PathInterpolatorCompat.create(0.22f, 1.0f, 0.36f, 1.0f))
.start()
if (willExpand) {
collapsedGroups.remove(category)
childrenContainer.visibility = View.VISIBLE
} else {
collapsedGroups.add(category)
childrenContainer.visibility = View.GONE
}
}
container.addView(headerView)
// 2. Add Alarm Rows under Group
for (alarm in groupAlarms) {
val item = createAlarmRow(
alarm.shiftType,
alarm.time,
alarm.isEnabled,
isCustom = true,
snoozeMin = alarm.snoozeInterval,
snoozeRepeat = alarm.snoozeRepeat,
soundUri = alarm.soundUri
) { isToggle, _ ->
if (isToggle) {
lifecycleScope.launch {
val enable = !alarm.isEnabled
AlarmSyncManager.toggleAlarm(requireContext(), alarm, enable)
refreshAlarmList()
}
} else {
showEditDialog("사용자 알람", alarm.time, alarm.shiftType, existingAlarm = alarm, isNew = false)
}
}
childrenContainer.addView(item)
}
container.addView(childrenContainer)
}
}
private fun createAlarmRow(
shiftName: String,
time: String,
isEnabled: Boolean,
isCustom: Boolean,
snoozeMin: Int,
snoozeRepeat: Int,
soundUri: String?,
onAction: (isToggle: Boolean, isLongClick: Boolean) -> Unit
): View {
val view = layoutInflater.inflate(R.layout.item_alarm_unified, binding.alarmListContainer, false)
view.isFocusable = true
val shiftIndicator = view.findViewById<TextView>(R.id.shiftIndicator)
val tvTime = view.findViewById<TextView>(R.id.tvTime)
val tvAmPm = view.findViewById<TextView>(R.id.tvAmPm)
val tvSummary = view.findViewById<TextView>(R.id.tvSummary)
val alarmSwitch = view.findViewById<MaterialSwitch>(R.id.alarmSwitch)
val layoutAlarmSwitch = view.findViewById<View>(R.id.layoutAlarmSwitch)
val shortName = when(shiftName) {
"주간" -> "주"
"석간" -> "석"
"야간" -> "야"
"주간 맞교대" -> "주맞"
"야간 맞교대" -> "야맞"
"기타" -> "기타"
else -> shiftName.take(1)
}
shiftIndicator.text = shortName
val colorRes = when(shiftName) {
"주간" -> R.color.shift_lemon
"석간" -> R.color.shift_seok
"야간" -> R.color.shift_ya
"주간 맞교대" -> R.color.shift_jumat
"야간 맞교대" -> R.color.shift_yamat
else -> R.color.shift_gray
}
val context = requireContext()
val color = ContextCompat.getColor(context, colorRes)
val drawable = ContextCompat.getDrawable(context, R.drawable.bg_shift_stroke_v4) as android.graphics.drawable.GradientDrawable
drawable.mutate()
drawable.setStroke(dpToPx(2.5f), color)
shiftIndicator.background = drawable
shiftIndicator.setTextColor(color)
try {
val parts = time.split(":")
val h24 = parts[0].toInt()
val m = parts[1].toInt()
val h12 = if (h24 % 12 == 0) 12 else h24 % 12
tvTime.text = String.format("%02d:%02d", h12, m)
tvAmPm.text = if (h24 < 12) "오전" else "오후"
if (!isEnabled) {
tvTime.setTextColor(ContextCompat.getColor(context, R.color.text_tertiary))
tvAmPm.setTextColor(ContextCompat.getColor(context, R.color.text_tertiary))
tvSummary.setTextColor(ContextCompat.getColor(context, R.color.text_tertiary))
shiftIndicator.alpha = 0.4f
} else {
tvTime.setTextColor(ContextCompat.getColor(context, R.color.text_primary))
tvAmPm.setTextColor(ContextCompat.getColor(context, R.color.text_secondary))
tvSummary.setTextColor(ContextCompat.getColor(context, R.color.primary))
shiftIndicator.alpha = 1.0f
}
} catch (e: Exception) { tvTime.text = time }
val tvSoundNameView = view.findViewById<TextView>(R.id.tvSoundName)
val soundName = getSoundTitle(context, soundUri)
tvSummary.text = "${snoozeMin}분 간격, ${if(snoozeRepeat == 99) "계속" else snoozeRepeat.toString() + "회"}"
tvSoundNameView.text = soundName
val rowContents = view.findViewById<View>(R.id.rowContents)
rowContents.setOnClickListener { onAction(false, false) }
rowContents.setOnLongClickListener { onAction(false, true); true }
alarmSwitch.isChecked = isEnabled
layoutAlarmSwitch.setOnClickListener {
// onAction will handle the data update and re-sync
onAction(true, false)
}
return view
}
private var currentDialogSoundUri: String? = null
private var tvSoundNameReference: android.widget.TextView? = null
/**
* 새 알람 추가 시 기본음으로 시스템 알람음 설정
* 무음 문제 해결을 위해 반드시 시스템 기본 알람음을 반환
*/
private fun getDefaultAlarmUri(context: Context): String {
// 1. 시스템 기본 알람음 (가장 우선)
val defaultUri = android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI
if (defaultUri != null) {
Log.d("ShiftAlarm", "시스템 기본 알람음 URI: $defaultUri")
return defaultUri.toString()
}
// 2. RingtoneManager에서 알람 타입 기본값 가져오기
val fallbackUri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_ALARM)
if (fallbackUri != null) {
Log.d("ShiftAlarm", "Fallback 알람음 URI: $fallbackUri")
return fallbackUri.toString()
}
// 3. 마지막 fallback: 알림음이라도 사용
val notificationUri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION)
if (notificationUri != null) {
Log.w("ShiftAlarm", "알람음 없음, 알림음 사용: $notificationUri")
return notificationUri.toString()
}
// 4. 최후의 수단: 벨소리
val ringtoneUri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_RINGTONE)
if (ringtoneUri != null) {
Log.w("ShiftAlarm", "알림음 없음, 벨소리 사용: $ringtoneUri")
return ringtoneUri.toString()
}
// 이 경우는 거의 없지만, 안전장치
Log.e("ShiftAlarm", "어떤 기본 소리도 찾을 수 없음")
return ""
}
private fun showEditDialog(
title: String, currentTime: String, shiftType: String, existingAlarm: CustomAlarm?, isNew: Boolean
) {
val dialogView = layoutInflater.inflate(R.layout.dialog_alarm_edit_spinner, null)
val dialog = AlertDialog.Builder(requireContext(), android.R.style.Theme_DeviceDefault_Light_NoActionBar_Fullscreen).setView(dialogView).create()
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
val tvTitle = dialogView.findViewById<TextView>(R.id.dialogTitle)
val timePicker = dialogView.findViewById<TimePicker>(R.id.timePicker)
val tvSoundName = dialogView.findViewById<TextView>(R.id.tvSoundName)
tvSoundNameReference = tvSoundName
val btnSelectSound = dialogView.findViewById<View>(R.id.btnSelectSound)
val btnDelete = dialogView.findViewById<Button>(R.id.btnDelete)
val btnCancel = dialogView.findViewById<View>(R.id.btnCancel)
val btnSave = dialogView.findViewById<View>(R.id.btnSave)
// Initialize Values
var selectedSnooze = existingAlarm?.snoozeInterval ?: 5
var selectedRepeat = existingAlarm?.snoozeRepeat ?: 3
// 새 알람 생성 시 기본적으로 시스템 알람음 설정 (무음 문제 해결)
// 기존 알람 수정 시에도 soundUri가 비어있거나 null이면 기본값으로 설정
val existingUri = existingAlarm?.soundUri
val isExistingUriEmpty = existingUri.isNullOrEmpty() || existingUri == "null"
currentDialogSoundUri = if (isNew || isExistingUriEmpty) {
// 새 알람 또는 기존 알람의 소리가 설정되지 않은 경우: 반드시 기본 알람음으로 설정
val defaultUri = getDefaultAlarmUri(requireContext())
Log.d("ShiftAlarm", "기본 알람음 설정: $defaultUri (isNew=$isNew, isExistingUriEmpty=$isExistingUriEmpty)")
defaultUri
} else {
// 기존 알람 수정: 기존 값 유지
existingUri
}
// soundUri가 비어있는 경우 최종 안전장치
if (currentDialogSoundUri.isNullOrEmpty()) {
currentDialogSoundUri = getDefaultAlarmUri(requireContext())
Log.w("ShiftAlarm", "soundUri가 비어있어 기본값으로 재설정: $currentDialogSoundUri")
}
// 새 알람 추가 시 또는 볼륨이 0인 경우 시스템 알람 스트림 볼륨을 최대치로 설정
try {
val audioManager = requireContext().getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager
val maxVol = audioManager.getStreamMaxVolume(android.media.AudioManager.STREAM_ALARM)
val currVol = audioManager.getStreamVolume(android.media.AudioManager.STREAM_ALARM)
if (isNew || currVol == 0) {
audioManager.setStreamVolume(android.media.AudioManager.STREAM_ALARM, maxVol, 0)
Log.d("ShiftAlarm", "알람 스트림 볼륨 최대치($maxVol)로 초기화 완료")
}
} catch (e: Exception) {
Log.w("ShiftAlarm", "알람 볼륨 설정 실패", e)
}
fun updateSoundName(uriStr: String?) {
if (uriStr.isNullOrEmpty() || uriStr == "null") {
tvSoundName.text = "기본 알람음"
} else {
try {
val uri = android.net.Uri.parse(uriStr)
val ringtone = android.media.RingtoneManager.getRingtone(requireContext(), uri)
val title = ringtone?.getTitle(requireContext()) ?: "알람음"
tvSoundName.text = title
} catch (e: Exception) {
tvSoundName.text = "알람음"
}
}
}
updateSoundName(currentDialogSoundUri)
// Snooze Interval Buttons
val snoozeButtons = listOf(
dialogView.findViewById<TextView>(R.id.snooze5),
dialogView.findViewById<TextView>(R.id.snooze10),
dialogView.findViewById<TextView>(R.id.snooze15),
dialogView.findViewById<TextView>(R.id.snooze30)
)
val snoozeValues = listOf(5, 10, 15, 30)
fun updateSnoozeUI() {
snoozeButtons.forEachIndexed { i, btn ->
val isSelected = snoozeValues[i] == selectedSnooze
btn.setBackgroundResource(if (isSelected) R.drawable.bg_pill_rect_selected else R.drawable.bg_pill_rect_unselected)
btn.setTextColor(if (isSelected) ContextCompat.getColor(requireContext(), R.color.white) else ContextCompat.getColor(requireContext(), R.color.text_secondary))
}
}
updateSnoozeUI()
snoozeButtons.forEachIndexed { i, btn -> btn.setOnClickListener { selectedSnooze = snoozeValues[i]; updateSnoozeUI() } }
// Repeat Count Buttons
val repeatButtons = listOf(
dialogView.findViewById<TextView>(R.id.repeat3),
dialogView.findViewById<TextView>(R.id.repeat5),
dialogView.findViewById<TextView>(R.id.repeatForever)
)
val repeatValues = listOf(3, 5, 99)
fun updateRepeatUI() {
repeatButtons.forEachIndexed { i, btn ->
val isSelected = repeatValues[i] == selectedRepeat
btn.setBackgroundResource(if (isSelected) R.drawable.bg_pill_rect_selected else R.drawable.bg_pill_rect_unselected)
btn.setTextColor(if (isSelected) ContextCompat.getColor(requireContext(), R.color.white) else ContextCompat.getColor(requireContext(), R.color.text_secondary))
}
}
updateRepeatUI()
repeatButtons.forEachIndexed { i, btn ->
btn.setOnClickListener {
selectedRepeat = repeatValues[i]
updateRepeatUI()
}
}
// Pre-Alarm Buttons
var selectedPreAlarm = 15
val preAlarmButtons = listOf(
dialogView.findViewById<TextView>(R.id.preAlarmOff),
dialogView.findViewById<TextView>(R.id.preAlarm5),
dialogView.findViewById<TextView>(R.id.preAlarm10),
dialogView.findViewById<TextView>(R.id.preAlarm15),
dialogView.findViewById<TextView>(R.id.preAlarm30)
)
val preAlarmValues = listOf(0, 5, 10, 15, 30)
fun updatePreAlarmUI() {
preAlarmButtons.forEachIndexed { i, btn ->
val isSelected = preAlarmValues[i] == selectedPreAlarm
btn.setBackgroundResource(if (isSelected) R.drawable.bg_pill_rect_selected else R.drawable.bg_pill_rect_unselected)
btn.setTextColor(if (isSelected) ContextCompat.getColor(requireContext(), R.color.white) else ContextCompat.getColor(requireContext(), R.color.text_secondary))
}
}
updatePreAlarmUI()
preAlarmButtons.forEachIndexed { i, btn -> btn.setOnClickListener { selectedPreAlarm = preAlarmValues[i]; updatePreAlarmUI() } }
val cardShift = dialogView.findViewById<View>(R.id.cardShiftSelector)
var currentShift = when(shiftType) {
"주", "주간" -> "주간"
"석", "석간" -> "석간"
"야", "야간" -> "야간"
"주맞", "주간 맞교대" -> "주간 맞교대"
"야맞", "야간 맞교대" -> "야간 맞교대"
else -> "기타"
}
cardShift.visibility = View.VISIBLE
val shiftBtns = listOf(
dialogView.findViewById<TextView>(R.id.btnShiftJu),
dialogView.findViewById<TextView>(R.id.btnShiftSeok),
dialogView.findViewById<TextView>(R.id.btnShiftYa),
dialogView.findViewById<TextView>(R.id.btnShiftJuMat),
dialogView.findViewById<TextView>(R.id.btnShiftYaMat),
dialogView.findViewById<TextView>(R.id.btnShiftEtc)
)
val shiftTypes = listOf("주간", "석간", "야간", "주간 맞교대", "야간 맞교대", "기타")
fun updateShiftUI() {
shiftBtns.forEachIndexed { i, btn ->
val isSelected = shiftTypes[i] == currentShift
val colorRes = when(shiftTypes[i]) {
"주간" -> R.color.shift_lemon
"석간" -> R.color.shift_seok
"야간" -> R.color.shift_ya
"주간 맞교대" -> R.color.shift_jumat
"야간 맞교대" -> R.color.shift_yamat
else -> R.color.shift_gray
}
val color = ContextCompat.getColor(requireContext(), colorRes)
if (isSelected) {
val d = ContextCompat.getDrawable(requireContext(), R.drawable.bg_shift_circle_v4) as android.graphics.drawable.GradientDrawable
d.mutate()
d.setColor(color)
btn.background = d
btn.setTextColor(if (shiftTypes[i] == "야간") ContextCompat.getColor(requireContext(), R.color.white) else ContextCompat.getColor(requireContext(), R.color.black))
} else {
val d = ContextCompat.getDrawable(requireContext(), R.drawable.bg_shift_stroke_v4) as android.graphics.drawable.GradientDrawable
d.mutate()
d.setStroke(dpToPx(1.5f), color)
btn.background = d
btn.setTextColor(color)
}
}
}
updateShiftUI()
shiftBtns.forEachIndexed { i, btn -> btn.setOnClickListener { currentShift = shiftTypes[i]; updateShiftUI() } }
tvTitle.text = if (isNew) "새 알람 추가" else "$shiftType 알람 수정"
timePicker.setIs24HourView(false)
val parts = currentTime.split(":")
if (android.os.Build.VERSION.SDK_INT >= 23) {
timePicker.hour = parts[0].toInt(); timePicker.minute = parts[1].toInt()
} else {
@Suppress("DEPRECATION")
timePicker.currentHour = parts[0].toInt()
@Suppress("DEPRECATION")
timePicker.currentMinute = parts[1].toInt()
}
btnSelectSound.setOnClickListener {
val intent = Intent(android.media.RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_TYPE, android.media.RingtoneManager.TYPE_ALARM)
putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, if (currentDialogSoundUri != null) android.net.Uri.parse(currentDialogSoundUri) else null as android.net.Uri?)
// 무음 선택 방지: 시스템 알람음만 선택 가능
putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
}
startActivityForResult(intent, 100)
}
if (!isNew) {
btnDelete.visibility = View.VISIBLE
btnDelete.setOnClickListener {
lifecycleScope.launch {
existingAlarm?.let {
// AlarmSyncManager를 사용하여 동기화된 삭제 수행
// DB 삭제 전 AlarmManager 취소가 보장됨
val result = AlarmSyncManager.deleteAlarm(requireContext(), it)
if (result.isSuccess) {
Log.d("ShiftAlarm", "알람 삭제 동기화 성공: ID=${it.id}")
} else {
Log.e("ShiftAlarm", "알람 삭제 동기화 실패", result.exceptionOrNull())
Toast.makeText(requireContext(), "알람 삭제 중 오류가 발생했습니다.", Toast.LENGTH_SHORT).show()
}
}
refreshAlarmList()
dialog.dismiss()
}
}
}
btnCancel.setOnClickListener { dialog.dismiss() }
btnSave.setOnClickListener {
@Suppress("DEPRECATION")
val h = if (android.os.Build.VERSION.SDK_INT >= 23) timePicker.hour else timePicker.currentHour
@Suppress("DEPRECATION")
val m = if (android.os.Build.VERSION.SDK_INT >= 23) timePicker.minute else timePicker.currentMinute
val time = String.format("%02d:%02d", h, m)
lifecycleScope.launch {
if (isNew) {
val newAlarm = CustomAlarm(
time = time,
shiftType = currentShift,
isEnabled = true,
soundUri = currentDialogSoundUri,
snoozeInterval = selectedSnooze,
snoozeRepeat = selectedRepeat
)
// AlarmSyncManager를 사용하여 동기화된 추가 수행
val result = AlarmSyncManager.addAlarm(requireContext(), newAlarm)
if (result.isSuccess) {
Log.d("ShiftAlarm", "새 알람 추가 동기화 성공")
Toast.makeText(requireContext(), "알람이 추가되었습니다.", Toast.LENGTH_SHORT).show()
} else {
Log.e("ShiftAlarm", "새 알람 추가 동기화 실패", result.exceptionOrNull())
Toast.makeText(requireContext(), "알람 추가 중 오류가 발생했습니다.", Toast.LENGTH_SHORT).show()
}
} else {
val updated = existingAlarm!!.copy(
time = time,
shiftType = currentShift,
soundUri = currentDialogSoundUri,
snoozeInterval = selectedSnooze,
snoozeRepeat = selectedRepeat
)
// AlarmSyncManager를 사용하여 동기화된 수정 수행
val result = AlarmSyncManager.updateAlarm(requireContext(), updated)
if (result.isSuccess) {
Log.d("ShiftAlarm", "알람 수정 동기화 성공: ID=${updated.id}")
Toast.makeText(requireContext(), "알람이 수정되었습니다.", Toast.LENGTH_SHORT).show()
} else {
Log.e("ShiftAlarm", "알람 수정 동기화 실패", result.exceptionOrNull())
Toast.makeText(requireContext(), "알람 수정 중 오류가 발생했습니다.", Toast.LENGTH_SHORT).show()
}
}
refreshAlarmList()
dialog.dismiss()
}
}
dialog.setOnDismissListener { tvSoundNameReference = null }
dialog.show()
}
private fun dpToPx(dp: Float): Int {
return (dp * resources.displayMetrics.density).toInt()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: android.content.Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == androidx.appcompat.app.AppCompatActivity.RESULT_OK) {
val uri = data?.getParcelableExtra<android.net.Uri>(android.media.RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
if (uri != null) {
currentDialogSoundUri = uri.toString()
try {
val ringtone = android.media.RingtoneManager.getRingtone(requireContext(), uri)
tvSoundNameReference?.text = ringtone.getTitle(requireContext())
} catch(e: Exception) {
tvSoundNameReference?.text = "사용자 지정음"
}
}
}
}
private fun getSoundTitle(context: Context, uriStr: String?): String {
if (soundTitleCache.containsKey(uriStr)) return soundTitleCache[uriStr]!!
// uriStr이 null이거나 비어있거나 "null" 문자열인 경우 기본음으로 처리
val title = if (uriStr.isNullOrEmpty() || uriStr == "null") {
"기본 알람음"
} else {
try {
val uri = android.net.Uri.parse(uriStr)
val ringtone = android.media.RingtoneManager.getRingtone(context, uri)
ringtone?.getTitle(context) ?: "알람음"
} catch (e: Exception) {
"알람음"
}
}
soundTitleCache[uriStr] = title
return title
}
}