Compare commits

..

3 Commits

Author SHA1 Message Date
PTah 6b4020adf1 fix: краш «Свой звук» на FragmentActivity (0.5.5)
Ringtone picker через startActivityForResult вместо Activity Result API с большим requestCode.
2026-06-14 18:32:44 +10:00
PTah f41a0d3a50 feat: настройка звука push-уведомлений (0.5.4)
Единый звук для каналов, выбор системный/свой в Устройстве, исправлен краш ringtone picker.
2026-06-14 18:29:21 +10:00
PTah 2aaea30468 chore: dev-заглушки полей подключения для обкатки (0.5.2)
Prefill URL, логина, пароля и имени устройства только в debug-сборке.
2026-06-14 17:51:11 +10:00
8 changed files with 41 additions and 53 deletions
-1
View File
@@ -22,7 +22,6 @@ 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 = 21 versionCode = 19
versionName = "0.5.7" versionName = "0.5.5"
} }
buildTypes { buildTypes {
@@ -59,22 +59,16 @@ 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.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(), ExternalPickerHost { class MainActivity : FragmentActivity() {
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)
@@ -105,12 +99,11 @@ class MainActivity : FragmentActivity(), ExternalPickerHost {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
RingtonePicker.handleActivityResult(this, requestCode, resultCode, data) RingtonePicker.handleActivityResult(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
@@ -119,10 +112,6 @@ class MainActivity : FragmentActivity(), ExternalPickerHost {
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(
@@ -1,8 +1,6 @@
package ru.kalinamall.seaca.push package ru.kalinamall.seaca.push
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent
import android.media.RingtoneManager import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import ru.kalinamall.seaca.data.NotificationSoundSettings import ru.kalinamall.seaca.data.NotificationSoundSettings
@@ -29,16 +27,4 @@ object NotificationSoundResolver {
"" ""
} }
} }
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.
}
}
} }
@@ -7,18 +7,15 @@ import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ru.kalinamall.seaca.push.NotificationSoundResolver import ru.kalinamall.seaca.push.NotificationSoundResolver
object RingtonePicker { object RingtonePicker {
private const val REQUEST_CODE = 9001 private const val REQUEST_CODE = 9001
private var callback: (suspend (Uri?) -> Unit)? = null private var callback: ((Uri?) -> Unit)? = null
fun launch(activity: FragmentActivity, currentUri: Uri, onResult: suspend (Uri?) -> Unit) { fun launch(activity: FragmentActivity, currentUri: Uri, onResult: (Uri?) -> Unit) {
callback = onResult callback = onResult
(activity as? ExternalPickerHost)?.beginExternalPickerFlow()
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
@@ -35,14 +32,12 @@ object RingtonePicker {
} }
} }
fun handleActivityResult(activity: FragmentActivity, requestCode: Int, resultCode: Int, data: Intent?) { fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CODE) return if (requestCode != REQUEST_CODE) return
val onResult = callback ?: return val onResult = callback ?: return
callback = null callback = null
val uri = if (resultCode == Activity.RESULT_OK) readResult(data) else null val uri = if (resultCode == Activity.RESULT_OK) readResult(data) else null
activity.lifecycleScope.launch { onResult(uri)
runCatching { onResult(uri) }
}
} }
private fun readResult(data: Intent?): Uri? { private fun readResult(data: Intent?): Uri? {
@@ -55,7 +50,3 @@ object RingtonePicker {
} }
} }
} }
interface ExternalPickerHost {
fun beginExternalPickerFlow()
}
@@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.kalinamall.seaca.BuildConfig
import ru.kalinamall.seaca.data.SacRepository import ru.kalinamall.seaca.data.SacRepository
@Composable @Composable
@@ -29,11 +30,20 @@ fun ConnectScreen(
repo: SacRepository, repo: SacRepository,
onConnected: () -> Unit, onConnected: () -> Unit,
) { ) {
var serverUrl by remember { mutableStateOf("") } val devDefaults = BuildConfig.DEBUG
var serverUrl by remember {
mutableStateOf(if (devDefaults) DevConnectDefaults.SERVER_URL else "")
}
var enrollmentCode by remember { mutableStateOf("") } var enrollmentCode by remember { mutableStateOf("") }
var username by remember { mutableStateOf("") } var username by remember {
var password by remember { mutableStateOf("") } mutableStateOf(if (devDefaults) DevConnectDefaults.USERNAME else "")
var displayName by remember { mutableStateOf(android.os.Build.MODEL) } }
var password by remember {
mutableStateOf(if (devDefaults) DevConnectDefaults.PASSWORD else "")
}
var displayName by remember {
mutableStateOf(if (devDefaults) DevConnectDefaults.DISPLAY_NAME else android.os.Build.MODEL)
}
var trustCert by remember { mutableStateOf(false) } var trustCert by remember { mutableStateOf(false) }
var loading by remember { mutableStateOf(false) } var loading by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
@@ -462,7 +462,7 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
val currentUri = NotificationSoundResolver.resolve(context, latestSoundSettings) val currentUri = NotificationSoundResolver.resolve(context, latestSoundSettings)
try { try {
RingtonePicker.launch(activity, currentUri) { uri -> RingtonePicker.launch(activity, currentUri) { uri ->
NotificationSoundResolver.persistCustomUri(context, uri) scope.launch {
if (uri == null) { if (uri == null) {
repo.session.setNotificationSound(NotificationSoundSource.SYSTEM, null) repo.session.setNotificationSound(NotificationSoundSource.SYSTEM, null)
} else { } else {
@@ -470,6 +470,7 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
} }
applySoundChannels(repo.session.getNotificationSoundSettings()) applySoundChannels(repo.session.getNotificationSoundSettings())
} }
}
} catch (_: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
Toast.makeText(context, "На устройстве нет выбора звука уведомлений", Toast.LENGTH_LONG).show() Toast.makeText(context, "На устройстве нет выбора звука уведомлений", Toast.LENGTH_LONG).show()
} }
@@ -0,0 +1,12 @@
package ru.kalinamall.seaca.ui.screens
/**
* Временные значения для обкатки. Удалить перед релизом.
* Используются только в debug-сборке ([ru.kalinamall.seaca.BuildConfig.DEBUG]).
*/
internal object DevConnectDefaults {
const val SERVER_URL = "https://sac-api.kalinamall.ru"
const val USERNAME = "admin"
const val PASSWORD = "R3tt3tnatur"
const val DISPLAY_NAME = "papas s25"
}