750 lines
34 KiB
Kotlin
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
|
|
}
|
|
}
|