Initial commit - v1.1.9

This commit is contained in:
2026-02-22 12:03:04 +09:00
commit 27339dc7b7
180 changed files with 12908 additions and 0 deletions

View File

@@ -0,0 +1,574 @@
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.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()
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()
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()
for (alarm in customAlarms) {
val item = createAlarmRow(alarm.shiftType, alarm.time, alarm.isEnabled, isCustom = true, snoozeMin = alarm.snoozeInterval, snoozeRepeat = alarm.snoozeRepeat, soundUri = alarm.soundUri) { isToggle, isLongOrShort ->
if (isToggle) {
// AlarmSyncManager를 사용하여 토글 동기화
lifecycleScope.launch {
val enable = !alarm.isEnabled
val result = AlarmSyncManager.toggleAlarm(requireContext(), alarm, enable)
if (result.isSuccess) {
Log.d("ShiftAlarm", "알람 토글 동기화 성공: ID=${alarm.id}, enabled=$enable")
} else {
Log.e("ShiftAlarm", "알람 토글 동기화 실패", result.exceptionOrNull())
Toast.makeText(requireContext(), "알람 상태 변경 중 오류가 발생했습니다.", Toast.LENGTH_SHORT).show()
}
refreshAlarmList()
}
} else {
showEditDialog("사용자 알람", alarm.time, alarm.shiftType, existingAlarm = alarm, isNew = false)
}
}
container.addView(item)
}
}
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")
}
Log.d("ShiftAlarm", "알람 ${if (isNew) "생성" else "수정"} - 최종 soundUri: $currentDialogSoundUri")
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() } }
val cardShift = dialogView.findViewById<View>(R.id.cardShiftSelector)
var currentShift = shiftType
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.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_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 {
timePicker.currentHour = parts[0].toInt(); 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?)
}
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 {
val h = if (android.os.Build.VERSION.SDK_INT >= 23) timePicker.hour else timePicker.currentHour
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
}
}