fix: persist custom push sound for FCM cold start (0.5.11)
Копирование ringtone во внутреннее хранилище, FileProvider, каналы с hash и миграция URI.
This commit is contained in:
@@ -38,5 +38,15 @@
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
package ru.kalinamall.seaca
|
||||
|
||||
import android.app.Application
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.kalinamall.seaca.data.SessionStore
|
||||
import ru.kalinamall.seaca.push.NotificationHelper
|
||||
|
||||
import ru.kalinamall.seaca.push.NotificationSoundResolver
|
||||
|
||||
class SeacaApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
NotificationHelper.ensureChannels(this, NotificationSoundResolver.defaultNotificationUri())
|
||||
val store = SessionStore(this)
|
||||
val settings = runBlocking {
|
||||
NotificationSoundResolver.normalizeStoredSettings(this@SeacaApplication, store)
|
||||
}
|
||||
NotificationHelper.ensureChannels(
|
||||
this,
|
||||
NotificationSoundResolver.resolve(this, settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,36 +13,47 @@ import ru.kalinamall.seaca.MainActivity
|
||||
import ru.kalinamall.seaca.R
|
||||
|
||||
object NotificationHelper {
|
||||
const val CHANNEL_DEFAULT = "sac_default"
|
||||
const val CHANNEL_HIGH = "sac_high"
|
||||
const val CHANNEL_CRITICAL = "sac_critical"
|
||||
const val CHANNEL_DEFAULT_SILENT = "sac_default_silent"
|
||||
const val CHANNEL_HIGH_SILENT = "sac_high_silent"
|
||||
const val CHANNEL_CRITICAL_SILENT = "sac_critical_silent"
|
||||
private const val CHANNEL_DEFAULT_SILENT = "sac_default_silent"
|
||||
private const val CHANNEL_HIGH_SILENT = "sac_high_silent"
|
||||
private const val CHANNEL_CRITICAL_SILENT = "sac_critical_silent"
|
||||
|
||||
private val legacySoundChannelIds = listOf("sac_default", "sac_high", "sac_critical")
|
||||
|
||||
private val soundChannelIds = listOf(CHANNEL_DEFAULT, CHANNEL_HIGH, CHANNEL_CRITICAL)
|
||||
private var appliedSoundKey: String? = null
|
||||
private var soundChannelSuffix: String = "sys"
|
||||
private var channelDefault = "sac_default_sys"
|
||||
private var channelHigh = "sac_high_sys"
|
||||
private var channelCritical = "sac_critical_sys"
|
||||
|
||||
fun ensureChannels(context: Context, soundUri: Uri = NotificationSoundResolver.defaultNotificationUri()) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val soundKey = soundUri.toString()
|
||||
val mgr = context.getSystemService(NotificationManager::class.java)
|
||||
if (soundKey != appliedSoundKey) {
|
||||
soundChannelIds.forEach { mgr.deleteNotificationChannel(it) }
|
||||
legacySoundChannelIds.forEach { mgr.deleteNotificationChannel(it) }
|
||||
if (appliedSoundKey != null) {
|
||||
mgr.deleteNotificationChannel(channelDefault)
|
||||
mgr.deleteNotificationChannel(channelHigh)
|
||||
mgr.deleteNotificationChannel(channelCritical)
|
||||
}
|
||||
appliedSoundKey = soundKey
|
||||
soundChannelSuffix = soundKey.hashCode().toUInt().toString(16).take(8)
|
||||
channelDefault = "sac_default_$soundChannelSuffix"
|
||||
channelHigh = "sac_high_$soundChannelSuffix"
|
||||
channelCritical = "sac_critical_$soundChannelSuffix"
|
||||
}
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.build()
|
||||
mgr.createNotificationChannel(
|
||||
soundChannel(context, CHANNEL_DEFAULT, R.string.channel_default, NotificationManager.IMPORTANCE_DEFAULT, soundUri, audioAttributes),
|
||||
soundChannel(context, channelDefault, R.string.channel_default, NotificationManager.IMPORTANCE_DEFAULT, soundUri, audioAttributes),
|
||||
)
|
||||
mgr.createNotificationChannel(
|
||||
soundChannel(context, CHANNEL_HIGH, R.string.channel_high, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
|
||||
soundChannel(context, channelHigh, R.string.channel_high, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
|
||||
)
|
||||
mgr.createNotificationChannel(
|
||||
soundChannel(context, CHANNEL_CRITICAL, R.string.channel_critical, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
|
||||
soundChannel(context, channelCritical, R.string.channel_critical, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
|
||||
)
|
||||
mgr.createNotificationChannel(silentChannel(context, CHANNEL_DEFAULT_SILENT, R.string.channel_default_silent))
|
||||
mgr.createNotificationChannel(silentChannel(context, CHANNEL_HIGH_SILENT, R.string.channel_high_silent))
|
||||
@@ -106,9 +117,9 @@ object NotificationHelper {
|
||||
.setPriority(
|
||||
when {
|
||||
silent -> NotificationCompat.PRIORITY_DEFAULT
|
||||
channel == CHANNEL_CRITICAL || channel == CHANNEL_CRITICAL_SILENT ->
|
||||
channel == channelCritical || channel == CHANNEL_CRITICAL_SILENT ->
|
||||
NotificationCompat.PRIORITY_MAX
|
||||
channel == CHANNEL_HIGH || channel == CHANNEL_HIGH_SILENT ->
|
||||
channel == channelHigh || channel == CHANNEL_HIGH_SILENT ->
|
||||
NotificationCompat.PRIORITY_HIGH
|
||||
else -> NotificationCompat.PRIORITY_DEFAULT
|
||||
},
|
||||
@@ -132,9 +143,9 @@ object NotificationHelper {
|
||||
}
|
||||
}
|
||||
return when (severity?.lowercase()) {
|
||||
"critical" -> CHANNEL_CRITICAL
|
||||
"high" -> CHANNEL_HIGH
|
||||
else -> CHANNEL_DEFAULT
|
||||
"critical" -> channelCritical
|
||||
"high" -> channelHigh
|
||||
else -> channelDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,37 @@ 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 -> {
|
||||
settings.customUri?.let { Uri.parse(it) } ?: defaultNotificationUri()
|
||||
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)
|
||||
|
||||
@@ -30,15 +48,62 @@ object NotificationSoundResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fun persistCustomUri(context: Context, uri: Uri?) {
|
||||
if (uri == null || uri.scheme != ContentResolver.SCHEME_CONTENT) return
|
||||
/**
|
||||
* 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) {
|
||||
// Picker may not grant persistable permission for some URIs.
|
||||
// Best-effort; internal copy is the reliable path.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ class SeacaMessagingService : FirebaseMessagingService() {
|
||||
val soundUri = if (silent) {
|
||||
NotificationSoundResolver.defaultNotificationUri()
|
||||
} else {
|
||||
val settings = runBlocking { repo.session.getNotificationSoundSettings() }
|
||||
val settings = runBlocking {
|
||||
NotificationSoundResolver.normalizeStoredSettings(applicationContext, repo.session)
|
||||
}
|
||||
NotificationSoundResolver.resolve(this, settings)
|
||||
}
|
||||
NotificationHelper.show(
|
||||
|
||||
@@ -472,9 +472,12 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(soundSettings) {
|
||||
LaunchedEffect(soundSettings, notificationMode) {
|
||||
if (notificationMode == NotificationMode.PUSH_SOUND) {
|
||||
applySoundChannels(soundSettings)
|
||||
val normalized = repo.session.let {
|
||||
NotificationSoundResolver.normalizeStoredSettings(context, it)
|
||||
}
|
||||
applySoundChannels(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,11 +489,15 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
|
||||
val currentUri = NotificationSoundResolver.resolve(context, latestSoundSettings)
|
||||
try {
|
||||
RingtonePicker.launch(activity, currentUri) { uri ->
|
||||
NotificationSoundResolver.persistCustomUri(context, uri)
|
||||
if (uri == null) {
|
||||
repo.session.setNotificationSound(NotificationSoundSource.SYSTEM, null)
|
||||
} else {
|
||||
repo.session.setNotificationSound(NotificationSoundSource.CUSTOM, uri.toString())
|
||||
val stored = NotificationSoundResolver.importCustomSound(context, uri)
|
||||
if (stored == null) {
|
||||
Toast.makeText(context, "Не удалось сохранить звук", Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
}
|
||||
repo.session.setNotificationSound(NotificationSoundSource.CUSTOM, stored)
|
||||
}
|
||||
applySoundChannels(repo.session.getNotificationSoundSettings())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path name="notification_sounds" path="." />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user