diff --git a/README.md b/README.md
index e1558a8..b62c3d7 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Android-клиент [Security Alert Center (SAC)](https://git.kalinamall.ru/PapaTramp/security-alert-center).
-**Версия:** `0.5.10` (versionCode 24)
+**Версия:** `0.5.11` (versionCode 25)
## Возможности
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 471795c..9b0da79 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -13,8 +13,8 @@ android {
applicationId = "ru.kalinamall.seaca"
minSdk = 26
targetSdk = 35
- versionCode = 24
- versionName = "0.5.10"
+ versionCode = 25
+ versionName = "0.5.11"
}
buildTypes {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 20b3375..7a175bd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,5 +38,15 @@
+
+
+
+
diff --git a/app/src/main/java/ru/kalinamall/seaca/SeacaApplication.kt b/app/src/main/java/ru/kalinamall/seaca/SeacaApplication.kt
index 004456c..8f57ddd 100644
--- a/app/src/main/java/ru/kalinamall/seaca/SeacaApplication.kt
+++ b/app/src/main/java/ru/kalinamall/seaca/SeacaApplication.kt
@@ -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),
+ )
}
}
diff --git a/app/src/main/java/ru/kalinamall/seaca/push/NotificationHelper.kt b/app/src/main/java/ru/kalinamall/seaca/push/NotificationHelper.kt
index 23d7262..703dfe3 100644
--- a/app/src/main/java/ru/kalinamall/seaca/push/NotificationHelper.kt
+++ b/app/src/main/java/ru/kalinamall/seaca/push/NotificationHelper.kt
@@ -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
}
}
}
diff --git a/app/src/main/java/ru/kalinamall/seaca/push/NotificationSoundResolver.kt b/app/src/main/java/ru/kalinamall/seaca/push/NotificationSoundResolver.kt
index cdae605..5cfa8b1 100644
--- a/app/src/main/java/ru/kalinamall/seaca/push/NotificationSoundResolver.kt
+++ b/app/src/main/java/ru/kalinamall/seaca/push/NotificationSoundResolver.kt
@@ -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.
}
}
}
diff --git a/app/src/main/java/ru/kalinamall/seaca/push/SeacaMessagingService.kt b/app/src/main/java/ru/kalinamall/seaca/push/SeacaMessagingService.kt
index fc5f636..9db72a1 100644
--- a/app/src/main/java/ru/kalinamall/seaca/push/SeacaMessagingService.kt
+++ b/app/src/main/java/ru/kalinamall/seaca/push/SeacaMessagingService.kt
@@ -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(
diff --git a/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt b/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt
index 03e9454..43ba56f 100644
--- a/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt
+++ b/app/src/main/java/ru/kalinamall/seaca/ui/screens/DetailScreens.kt
@@ -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())
}
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..4698997
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+