Files
seaca/app/src/main/java/ru/kalinamall/seaca/push/NotificationSoundResolver.kt
T
PTah 6a0758ffad fix: persist custom push sound for FCM cold start (0.5.11)
Копирование ringtone во внутреннее хранилище, FileProvider, каналы с hash и миграция URI.
2026-06-21 10:46:18 +10:00

110 lines
4.2 KiB
Kotlin

package ru.kalinamall.seaca.push
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import androidx.core.content.FileProvider
import ru.kalinamall.seaca.data.NotificationSoundSettings
import ru.kalinamall.seaca.data.NotificationSoundSource
import ru.kalinamall.seaca.data.SessionStore
import java.io.File
object NotificationSoundResolver {
private const val CUSTOM_SOUND_FILE = "custom_notification_sound"
fun resolve(context: Context, settings: NotificationSoundSettings): Uri {
return when (settings.source) {
NotificationSoundSource.SYSTEM -> defaultNotificationUri()
NotificationSoundSource.CUSTOM -> {
playableCustomUri(context, settings.customUri) ?: defaultNotificationUri()
}
}
}
suspend fun normalizeStoredSettings(context: Context, store: SessionStore): NotificationSoundSettings {
val settings = store.getNotificationSoundSettings()
if (settings.source != NotificationSoundSource.CUSTOM || settings.customUri.isNullOrBlank()) {
return settings
}
if (isInternalSoundUri(context, settings.customUri)) {
return settings
}
val imported = importCustomSound(context, Uri.parse(settings.customUri)) ?: return settings
store.setNotificationSound(NotificationSoundSource.CUSTOM, imported)
return store.getNotificationSoundSettings()
}
fun defaultNotificationUri(): Uri =
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
fun displayName(context: Context, settings: NotificationSoundSettings): String {
return try {
val uri = resolve(context, settings)
RingtoneManager.getRingtone(context, uri)?.getTitle(context) ?: ""
} catch (_: Exception) {
""
}
}
/**
* Copies picker/system URI into app storage so FCM in a fresh process can read it.
* Returns stored URI string (FileProvider or android.resource).
*/
fun importCustomSound(context: Context, source: Uri): String? {
if (ContentResolver.SCHEME_ANDROID_RESOURCE == source.scheme) {
return source.toString()
}
if (defaultNotificationUri().toString() == source.toString()) {
return source.toString()
}
persistCustomUri(context, source)
val dest = File(context.filesDir, CUSTOM_SOUND_FILE)
return try {
context.contentResolver.openInputStream(source)?.use { input ->
dest.outputStream().use { output -> input.copyTo(output) }
} ?: return null
internalFileUri(context, dest).toString()
} catch (_: Exception) {
null
}
}
private fun playableCustomUri(context: Context, stored: String?): Uri? {
if (stored.isNullOrBlank()) return null
val parsed = Uri.parse(stored)
if (isInternalSoundUri(context, stored)) {
val file = File(context.filesDir, CUSTOM_SOUND_FILE)
return if (file.isFile) internalFileUri(context, file) else null
}
if (ContentResolver.SCHEME_ANDROID_RESOURCE == parsed.scheme) {
return parsed
}
return importCustomSound(context, parsed)?.let { Uri.parse(it) }
}
private fun isInternalSoundUri(context: Context, stored: String): Boolean {
val parsed = Uri.parse(stored)
return parsed.authority == fileProviderAuthority(context)
}
private fun internalFileUri(context: Context, file: File): Uri =
FileProvider.getUriForFile(context, fileProviderAuthority(context), file)
private fun fileProviderAuthority(context: Context): String =
"${context.packageName}.fileprovider"
private fun persistCustomUri(context: Context, uri: Uri) {
if (uri.scheme != ContentResolver.SCHEME_CONTENT) return
try {
context.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION,
)
} catch (_: SecurityException) {
// Best-effort; internal copy is the reliable path.
}
}
}