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 = mutableListOf() private val collapsedGroups = mutableSetOf() 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() 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>() 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(R.id.ivExpand) val tvGroupName = headerView.findViewById(R.id.tvGroupName) val tvAlarmCount = headerView.findViewById(R.id.tvAlarmCount) val groupSwitch = headerView.findViewById(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(R.id.shiftIndicator) val tvTime = view.findViewById(R.id.tvTime) val tvAmPm = view.findViewById(R.id.tvAmPm) val tvSummary = view.findViewById(R.id.tvSummary) val alarmSwitch = view.findViewById(R.id.alarmSwitch) val layoutAlarmSwitch = view.findViewById(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(R.id.tvSoundName) val soundName = getSoundTitle(context, soundUri) tvSummary.text = "${snoozeMin}분 간격, ${if(snoozeRepeat == 99) "계속" else snoozeRepeat.toString() + "회"}" tvSoundNameView.text = soundName val rowContents = view.findViewById(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(R.id.dialogTitle) val timePicker = dialogView.findViewById(R.id.timePicker) val tvSoundName = dialogView.findViewById(R.id.tvSoundName) tvSoundNameReference = tvSoundName val btnSelectSound = dialogView.findViewById(R.id.btnSelectSound) val btnDelete = dialogView.findViewById