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 @@ + + + +