Compare commits

...

4 Commits

Author SHA1 Message Date
PTah 642d468005 fix: возврат из picker звука без краша, убраны dev-credentials (0.5.7)
Не блокировать биометрией при выборе звука. Удалён DevConnectDefaults.kt с паролем из репозитория.
2026-06-14 18:38:08 +10:00
PTah 37c8357a87 fix: краш «Свой звук» на FragmentActivity (0.5.5)
Ringtone picker через startActivityForResult вместо Activity Result API с большим requestCode.
2026-06-14 18:32:44 +10:00
PTah 902e47880d feat: настройка звука push-уведомлений (0.5.4)
Единый звук для каналов, выбор системный/свой в Устройстве, исправлен краш ringtone picker.
2026-06-14 18:29:21 +10:00
PTah 16ce2f94bb chore: dev-заглушки полей подключения для обкатки (0.5.2)
Prefill URL, логина, пароля и имени устройства только в debug-сборке.
2026-06-14 17:51:11 +10:00
12 changed files with 342 additions and 21 deletions
+1
View File
@@ -22,6 +22,7 @@ google-services.json
*.jks *.jks
local.properties local.properties
secrets.properties secrets.properties
app/src/main/java/ru/kalinamall/seaca/ui/screens/DevConnectDefaults.kt
# Env # Env
.env .env
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "ru.kalinamall.seaca" applicationId = "ru.kalinamall.seaca"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 15 versionCode = 21
versionName = "0.5.1" versionName = "0.5.7"
} }
buildTypes { buildTypes {
@@ -59,15 +59,22 @@ import ru.kalinamall.seaca.ui.screens.HostDetailScreen
import ru.kalinamall.seaca.ui.screens.HostsScreen import ru.kalinamall.seaca.ui.screens.HostsScreen
import ru.kalinamall.seaca.ui.screens.ProblemDetailScreen import ru.kalinamall.seaca.ui.screens.ProblemDetailScreen
import ru.kalinamall.seaca.ui.screens.ProblemsScreen import ru.kalinamall.seaca.ui.screens.ProblemsScreen
import ru.kalinamall.seaca.ui.ExternalPickerHost
import ru.kalinamall.seaca.ui.RingtonePicker
import ru.kalinamall.seaca.ui.screens.ReportsScreen import ru.kalinamall.seaca.ui.screens.ReportsScreen
import ru.kalinamall.seaca.ui.theme.SeacaTheme import ru.kalinamall.seaca.ui.theme.SeacaTheme
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity(), ExternalPickerHost {
private lateinit var repo: SacRepository private lateinit var repo: SacRepository
private val biometricUnlocked = mutableStateOf(true) private val biometricUnlocked = mutableStateOf(true)
private var pendingBiometric = false private var pendingBiometric = false
private var externalPickerFlowActive = false
private val deepLinkState = composeMutableStateOf<String?>(null) private val deepLinkState = composeMutableStateOf<String?>(null)
override fun beginExternalPickerFlow() {
externalPickerFlowActive = true
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
repo = SacRepository(applicationContext) repo = SacRepository(applicationContext)
@@ -94,8 +101,16 @@ class MainActivity : FragmentActivity() {
deepLinkState.value = extractDeepLink(intent) deepLinkState.value = extractDeepLink(intent)
} }
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
RingtonePicker.handleActivityResult(this, requestCode, resultCode, data)
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if (externalPickerFlowActive) return
if (shouldRequireBiometric()) { if (shouldRequireBiometric()) {
biometricUnlocked.value = false biometricUnlocked.value = false
pendingBiometric = true pendingBiometric = true
@@ -104,6 +119,10 @@ class MainActivity : FragmentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (externalPickerFlowActive) {
externalPickerFlowActive = false
return
}
if (pendingBiometric && !biometricUnlocked.value && repo.session.getAccessToken() != null) { if (pendingBiometric && !biometricUnlocked.value && repo.session.getAccessToken() != null) {
pendingBiometric = false pendingBiometric = false
BiometricGate.authenticate( BiometricGate.authenticate(
@@ -3,9 +3,11 @@ package ru.kalinamall.seaca
import android.app.Application import android.app.Application
import ru.kalinamall.seaca.push.NotificationHelper import ru.kalinamall.seaca.push.NotificationHelper
import ru.kalinamall.seaca.push.NotificationSoundResolver
class SeacaApplication : Application() { class SeacaApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
NotificationHelper.ensureChannels(this) NotificationHelper.ensureChannels(this, NotificationSoundResolver.defaultNotificationUri())
} }
} }
@@ -0,0 +1,20 @@
package ru.kalinamall.seaca.data
enum class NotificationSoundSource(
val storageKey: String,
val label: String,
) {
SYSTEM("system", "Системный звук"),
CUSTOM("custom", "Свой звук"),
;
companion object {
fun fromStorage(value: String?): NotificationSoundSource =
entries.firstOrNull { it.storageKey == value } ?: SYSTEM
}
}
data class NotificationSoundSettings(
val source: NotificationSoundSource,
val customUri: String?,
)
@@ -40,12 +40,27 @@ class SessionStore(private val context: Context) {
DateTimeDisplayMode.fromStorage(prefs[KEY_DATE_TIME_FORMAT]) DateTimeDisplayMode.fromStorage(prefs[KEY_DATE_TIME_FORMAT])
} }
val notificationSoundSettings: Flow<NotificationSoundSettings> = context.dataStore.data.map { prefs ->
NotificationSoundSettings(
source = NotificationSoundSource.fromStorage(prefs[KEY_NOTIFICATION_SOUND_SOURCE]),
customUri = prefs[KEY_NOTIFICATION_SOUND_URI],
)
}
suspend fun getNotificationMode(): NotificationMode = suspend fun getNotificationMode(): NotificationMode =
NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE]) NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE])
suspend fun getDateTimeDisplayMode(): DateTimeDisplayMode = suspend fun getDateTimeDisplayMode(): DateTimeDisplayMode =
DateTimeDisplayMode.fromStorage(context.dataStore.data.first()[KEY_DATE_TIME_FORMAT]) DateTimeDisplayMode.fromStorage(context.dataStore.data.first()[KEY_DATE_TIME_FORMAT])
suspend fun getNotificationSoundSettings(): NotificationSoundSettings {
val prefs = context.dataStore.data.first()
return NotificationSoundSettings(
source = NotificationSoundSource.fromStorage(prefs[KEY_NOTIFICATION_SOUND_SOURCE]),
customUri = prefs[KEY_NOTIFICATION_SOUND_URI],
)
}
suspend fun setNotificationMode(mode: NotificationMode) { suspend fun setNotificationMode(mode: NotificationMode) {
context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey } context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey }
} }
@@ -54,6 +69,17 @@ class SessionStore(private val context: Context) {
context.dataStore.edit { it[KEY_DATE_TIME_FORMAT] = mode.storageKey } context.dataStore.edit { it[KEY_DATE_TIME_FORMAT] = mode.storageKey }
} }
suspend fun setNotificationSound(source: NotificationSoundSource, customUri: String?) {
context.dataStore.edit {
it[KEY_NOTIFICATION_SOUND_SOURCE] = source.storageKey
if (customUri.isNullOrBlank()) {
it.remove(KEY_NOTIFICATION_SOUND_URI)
} else {
it[KEY_NOTIFICATION_SOUND_URI] = customUri
}
}
}
suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL] suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL]
suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false
@@ -114,6 +140,8 @@ class SessionStore(private val context: Context) {
private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs") private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs")
private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode") private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode")
private val KEY_DATE_TIME_FORMAT = stringPreferencesKey("date_time_format") private val KEY_DATE_TIME_FORMAT = stringPreferencesKey("date_time_format")
private val KEY_NOTIFICATION_SOUND_SOURCE = stringPreferencesKey("notification_sound_source")
private val KEY_NOTIFICATION_SOUND_URI = stringPreferencesKey("notification_sound_uri")
private const val KEY_ACCESS = "access_token" private const val KEY_ACCESS = "access_token"
private const val KEY_REFRESH = "refresh_token" private const val KEY_REFRESH = "refresh_token"
private const val KEY_DEVICE_ID = "device_id" private const val KEY_DEVICE_ID = "device_id"
@@ -5,6 +5,8 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import ru.kalinamall.seaca.MainActivity import ru.kalinamall.seaca.MainActivity
@@ -18,35 +20,47 @@ object NotificationHelper {
const val CHANNEL_HIGH_SILENT = "sac_high_silent" const val CHANNEL_HIGH_SILENT = "sac_high_silent"
const val CHANNEL_CRITICAL_SILENT = "sac_critical_silent" const val CHANNEL_CRITICAL_SILENT = "sac_critical_silent"
fun ensureChannels(context: Context) { private val soundChannelIds = listOf(CHANNEL_DEFAULT, CHANNEL_HIGH, CHANNEL_CRITICAL)
private var appliedSoundKey: String? = null
fun ensureChannels(context: Context, soundUri: Uri = NotificationSoundResolver.defaultNotificationUri()) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val soundKey = soundUri.toString()
val mgr = context.getSystemService(NotificationManager::class.java) val mgr = context.getSystemService(NotificationManager::class.java)
if (soundKey != appliedSoundKey) {
soundChannelIds.forEach { mgr.deleteNotificationChannel(it) }
appliedSoundKey = soundKey
}
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
mgr.createNotificationChannel( mgr.createNotificationChannel(
NotificationChannel( soundChannel(context, CHANNEL_DEFAULT, R.string.channel_default, NotificationManager.IMPORTANCE_DEFAULT, soundUri, audioAttributes),
CHANNEL_DEFAULT,
context.getString(R.string.channel_default),
NotificationManager.IMPORTANCE_DEFAULT,
),
) )
mgr.createNotificationChannel( mgr.createNotificationChannel(
NotificationChannel( soundChannel(context, CHANNEL_HIGH, R.string.channel_high, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
CHANNEL_HIGH,
context.getString(R.string.channel_high),
NotificationManager.IMPORTANCE_HIGH,
),
) )
mgr.createNotificationChannel( mgr.createNotificationChannel(
NotificationChannel( soundChannel(context, CHANNEL_CRITICAL, R.string.channel_critical, NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributes),
CHANNEL_CRITICAL,
context.getString(R.string.channel_critical),
NotificationManager.IMPORTANCE_HIGH,
),
) )
mgr.createNotificationChannel(silentChannel(context, CHANNEL_DEFAULT_SILENT, R.string.channel_default_silent)) mgr.createNotificationChannel(silentChannel(context, CHANNEL_DEFAULT_SILENT, R.string.channel_default_silent))
mgr.createNotificationChannel(silentChannel(context, CHANNEL_HIGH_SILENT, R.string.channel_high_silent)) mgr.createNotificationChannel(silentChannel(context, CHANNEL_HIGH_SILENT, R.string.channel_high_silent))
mgr.createNotificationChannel(silentChannel(context, CHANNEL_CRITICAL_SILENT, R.string.channel_critical_silent)) mgr.createNotificationChannel(silentChannel(context, CHANNEL_CRITICAL_SILENT, R.string.channel_critical_silent))
} }
private fun soundChannel(
context: Context,
id: String,
nameRes: Int,
importance: Int,
soundUri: Uri,
audioAttributes: AudioAttributes,
): NotificationChannel = NotificationChannel(id, context.getString(nameRes), importance).apply {
setSound(soundUri, audioAttributes)
enableVibration(true)
}
private fun silentChannel(context: Context, id: String, nameRes: Int): NotificationChannel = private fun silentChannel(context: Context, id: String, nameRes: Int): NotificationChannel =
NotificationChannel(id, context.getString(nameRes), NotificationManager.IMPORTANCE_DEFAULT).apply { NotificationChannel(id, context.getString(nameRes), NotificationManager.IMPORTANCE_DEFAULT).apply {
setSound(null, null) setSound(null, null)
@@ -66,8 +80,9 @@ object NotificationHelper {
kind: String?, kind: String?,
id: String?, id: String?,
silent: Boolean = false, silent: Boolean = false,
soundUri: Uri = NotificationSoundResolver.defaultNotificationUri(),
) { ) {
ensureChannels(context) ensureChannels(context, soundUri)
val channel = resolveChannel(severity, silent) val channel = resolveChannel(severity, silent)
val intent = Intent(context, MainActivity::class.java).apply { val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -0,0 +1,44 @@
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 ru.kalinamall.seaca.data.NotificationSoundSettings
import ru.kalinamall.seaca.data.NotificationSoundSource
object NotificationSoundResolver {
fun resolve(context: Context, settings: NotificationSoundSettings): Uri {
return when (settings.source) {
NotificationSoundSource.SYSTEM -> defaultNotificationUri()
NotificationSoundSource.CUSTOM -> {
settings.customUri?.let { Uri.parse(it) } ?: defaultNotificationUri()
}
}
}
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) {
""
}
}
fun persistCustomUri(context: Context, uri: Uri?) {
if (uri == null || 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.
}
}
}
@@ -27,6 +27,12 @@ class SeacaMessagingService : FirebaseMessagingService() {
val data = message.data val data = message.data
val title = data["title"] ?: message.notification?.title ?: "SAC" val title = data["title"] ?: message.notification?.title ?: "SAC"
val body = data["body"] ?: message.notification?.body ?: "" val body = data["body"] ?: message.notification?.body ?: ""
val soundUri = if (silent) {
NotificationSoundResolver.defaultNotificationUri()
} else {
val settings = runBlocking { repo.session.getNotificationSoundSettings() }
NotificationSoundResolver.resolve(this, settings)
}
NotificationHelper.show( NotificationHelper.show(
context = this, context = this,
title = title, title = title,
@@ -35,6 +41,7 @@ class SeacaMessagingService : FirebaseMessagingService() {
kind = data["kind"], kind = data["kind"],
id = data["id"], id = data["id"],
silent = silent, silent = silent,
soundUri = soundUri,
) )
} }
@@ -0,0 +1,61 @@
package ru.kalinamall.seaca.ui
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ru.kalinamall.seaca.push.NotificationSoundResolver
object RingtonePicker {
private const val REQUEST_CODE = 9001
private var callback: (suspend (Uri?) -> Unit)? = null
fun launch(activity: FragmentActivity, currentUri: Uri, onResult: suspend (Uri?) -> Unit) {
callback = onResult
(activity as? ExternalPickerHost)?.beginExternalPickerFlow()
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, NotificationSoundResolver.defaultNotificationUri())
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentUri)
}
try {
@Suppress("DEPRECATION")
activity.startActivityForResult(intent, REQUEST_CODE)
} catch (_: ActivityNotFoundException) {
callback = null
throw ActivityNotFoundException()
}
}
fun handleActivityResult(activity: FragmentActivity, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CODE) return
val onResult = callback ?: return
callback = null
val uri = if (resultCode == Activity.RESULT_OK) readResult(data) else null
activity.lifecycleScope.launch {
runCatching { onResult(uri) }
}
}
private fun readResult(data: Intent?): Uri? {
if (data == null) return null
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java)
} else {
@Suppress("DEPRECATION")
data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
}
}
interface ExternalPickerHost {
fun beginExternalPickerFlow()
}
@@ -1,5 +1,10 @@
package ru.kalinamall.seaca.ui.screens package ru.kalinamall.seaca.ui.screens
import android.content.ActivityNotFoundException
import android.widget.Toast
import androidx.compose.runtime.rememberUpdatedState
import androidx.fragment.app.FragmentActivity
import android.media.RingtoneManager
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -35,6 +40,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -46,8 +52,13 @@ import ru.kalinamall.seaca.data.HostDetail
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.kalinamall.seaca.data.DateTimeDisplayMode import ru.kalinamall.seaca.data.DateTimeDisplayMode
import ru.kalinamall.seaca.data.NotificationMode import ru.kalinamall.seaca.data.NotificationMode
import ru.kalinamall.seaca.data.NotificationSoundSettings
import ru.kalinamall.seaca.data.NotificationSoundSource
import ru.kalinamall.seaca.data.ProblemDetail import ru.kalinamall.seaca.data.ProblemDetail
import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.data.SacRepository
import ru.kalinamall.seaca.push.NotificationHelper
import ru.kalinamall.seaca.push.NotificationSoundResolver
import ru.kalinamall.seaca.ui.RingtonePicker
import ru.kalinamall.seaca.ui.components.BackArrowButton import ru.kalinamall.seaca.ui.components.BackArrowButton
import ru.kalinamall.seaca.ui.components.ForwardArrowButton import ru.kalinamall.seaca.ui.components.ForwardArrowButton
import ru.kalinamall.seaca.ui.util.SacDateTimes import ru.kalinamall.seaca.ui.util.SacDateTimes
@@ -418,11 +429,55 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
var username by remember { mutableStateOf<String?>(null) } var username by remember { mutableStateOf<String?>(null) }
var role by remember { mutableStateOf<String?>(null) } var role by remember { mutableStateOf<String?>(null) }
var deviceId by remember { mutableStateOf(0) } var deviceId by remember { mutableStateOf(0) }
val context = LocalContext.current
val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND) val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND)
val soundSettings by repo.session.notificationSoundSettings.collectAsStateWithLifecycle(
initialValue = NotificationSoundSettings(NotificationSoundSource.SYSTEM, null),
)
val dateTimeMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle( val dateTimeMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
initialValue = DateTimeDisplayMode.SLASH, initialValue = DateTimeDisplayMode.SLASH,
) )
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val activity = context as? FragmentActivity
val latestSoundSettings by rememberUpdatedState(soundSettings)
suspend fun applySoundChannels(settings: NotificationSoundSettings) {
NotificationHelper.ensureChannels(
context,
NotificationSoundResolver.resolve(context, settings),
)
}
LaunchedEffect(soundSettings) {
if (notificationMode == NotificationMode.PUSH_SOUND) {
applySoundChannels(soundSettings)
}
}
fun openRingtonePicker() {
if (activity == null) {
Toast.makeText(context, "Не удалось открыть выбор звука", Toast.LENGTH_SHORT).show()
return
}
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())
}
applySoundChannels(repo.session.getNotificationSoundSettings())
}
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, "На устройстве нет выбора звука уведомлений", Toast.LENGTH_LONG).show()
}
}
fun previewSound() {
RingtoneManager.getRingtone(context, NotificationSoundResolver.resolve(context, soundSettings))?.play()
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
baseUrl = repo.session.getBaseUrl() baseUrl = repo.session.getBaseUrl()
@@ -482,6 +537,71 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
} }
} }
if (notificationMode == NotificationMode.PUSH_SOUND) {
Text(
stringResource(R.string.notification_sound_title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 8.dp),
)
NotificationSoundSource.entries.forEach { source ->
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = soundSettings.source == source,
onClick = {
when (source) {
NotificationSoundSource.SYSTEM -> {
scope.launch {
repo.session.setNotificationSound(source, null)
applySoundChannels(repo.session.getNotificationSoundSettings())
}
}
NotificationSoundSource.CUSTOM -> {
if (soundSettings.customUri.isNullOrBlank()) {
openRingtonePicker()
} else {
scope.launch {
repo.session.setNotificationSound(source, soundSettings.customUri)
applySoundChannels(repo.session.getNotificationSoundSettings())
}
}
}
}
},
)
Text(
source.label,
modifier = Modifier.padding(start = 8.dp),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
Text(
stringResource(
R.string.notification_sound_current,
NotificationSoundResolver.displayName(context, soundSettings),
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (soundSettings.source == NotificationSoundSource.CUSTOM) {
Button(onClick = { openRingtonePicker() }) {
Text(stringResource(R.string.notification_sound_pick))
}
}
Button(onClick = { previewSound() }) {
Text(stringResource(R.string.notification_sound_preview))
}
}
}
Text( Text(
stringResource(R.string.date_time_format_title), stringResource(R.string.date_time_format_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
+4
View File
@@ -9,4 +9,8 @@
<string name="channel_critical_silent">SAC: критические (без звука)</string> <string name="channel_critical_silent">SAC: критические (без звука)</string>
<string name="notification_mode_title">Push-уведомления</string> <string name="notification_mode_title">Push-уведомления</string>
<string name="date_time_format_title">Формат даты и времени</string> <string name="date_time_format_title">Формат даты и времени</string>
<string name="notification_sound_title">Звук уведомлений</string>
<string name="notification_sound_pick">Выбрать звук</string>
<string name="notification_sound_preview">Проверить звук</string>
<string name="notification_sound_current">Сейчас: %1$s</string>
</resources> </resources>