From d516c8ce1853da2c945bfaa2e9a5e88a3ad6ee84 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 21 Jun 2026 20:18:53 +0300 Subject: [PATCH] Added Block class and BlockingScreen --- .../kotlin/com/tutpro/baresip/BlockRule.kt | 49 +++ .../com/tutpro/baresip/BlockingScreen.kt | 382 ++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 app/src/main/kotlin/com/tutpro/baresip/BlockRule.kt create mode 100644 app/src/main/kotlin/com/tutpro/baresip/BlockingScreen.kt diff --git a/app/src/main/kotlin/com/tutpro/baresip/BlockRule.kt b/app/src/main/kotlin/com/tutpro/baresip/BlockRule.kt new file mode 100644 index 00000000..5bd9a41a --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/BlockRule.kt @@ -0,0 +1,49 @@ +package com.tutpro.baresip + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.io.File + +@Serializable +class BlockRule( + val pattern: String +) { + fun matches(uri: String): Boolean { + if (uri.contains(pattern, ignoreCase = true)) return true + return try { + Regex(pattern, RegexOption.IGNORE_CASE).containsMatchIn(uri) + } catch (_: Exception) { + false + } + } + + companion object { + fun exists(pattern: String): Boolean { + return BaresipService.blockRules.any { it.pattern == pattern } + } + + fun save() { + Log.d(TAG, "Saving ${BaresipService.blockRules.size} block rules") + val file = File(BaresipService.filesPath + "/blocking.json") + try { + val jsonString = Json.encodeToString(BaresipService.blockRules) + file.writeText(jsonString) + } catch (e: Exception) { + Log.e(TAG, "Serialization exception", e) + } + } + + fun restore() { + val file = File(BaresipService.filesPath + "/blocking.json") + if (file.exists()) + try { + val jsonString = file.readText() + BaresipService.blockRules = Json.decodeFromString>(jsonString) + Log.d(TAG, "Restored ${BaresipService.blockRules.size} block rules") + } catch (e: Exception) { + Log.e(TAG, "Deserialization exception: $e") + } + } + + } +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/BlockingScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/BlockingScreen.kt new file mode 100644 index 00000000..e6c1661d --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/BlockingScreen.kt @@ -0,0 +1,382 @@ +package com.tutpro.baresip + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.tutpro.baresip.CustomElements.AlertDialog +import com.tutpro.baresip.CustomElements.verticalScrollbar + +fun NavGraphBuilder.blockingScreenRoute(navController: NavController) { + composable( + route = "blocking/{aor}", + arguments = listOf(navArgument("aor") { type = NavType.StringType }) + ) { backStackEntry -> + val aor = backStackEntry.arguments?.getString("aor")!! + val viewModel = viewModel() + val ua = UserAgent.ofAor(aor)!! + BlockingScreen(navController, viewModel, ua) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BlockingScreen(navController: NavController, viewModel: AccountViewModel, ua: UserAgent) { + val acc = ua.account + var rules by remember { mutableStateOf(BaresipService.blockRules.toList()) } + + remember { + viewModel.loadAccount(acc) + true + } + + Scaffold( + modifier = Modifier.fillMaxSize().imePadding(), + containerColor = MaterialTheme.colorScheme.background, + topBar = { + Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { + Spacer(Modifier.statusBarsPadding()) + TopAppBar( + title = { + Text(text = stringResource(R.string.blocking), fontWeight = FontWeight.Bold) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + ), + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + windowInsets = WindowInsets(0, 0, 0, 0) + ) + } + }, + bottomBar = { NewRule(onRuleAdded = { rules = BaresipService.blockRules.toList() }) }, + content = { contentPadding -> + BlockingContent( + contentPadding, + viewModel, + rules, + acc, + onRuleDeleted = { rules = BaresipService.blockRules.toList() } + ) + }, + ) +} + +@Composable +fun BlockingContent( + contentPadding: PaddingValues, + viewModel: AccountViewModel, + rules: List, + acc: Account, + onRuleDeleted: () -> Unit +) { + val ctx = LocalContext.current + val alertTitle = remember { mutableStateOf("") } + val alertMessage = remember { mutableStateOf("") } + val showAlert = remember { mutableStateOf(false) } + + if (showAlert.value) + AlertDialog( + showDialog = showAlert, + title = alertTitle.value, + message = alertMessage.value, + lastButtonText = stringResource(R.string.ok), + ) + + @Composable + fun BlockUnknown() { + val blockUnknownTitle = stringResource(R.string.block_unknown) + val blockUnknownHelp = stringResource(R.string.block_unknown_help) + val block by viewModel.blockUnknown.collectAsState() + Row( + Modifier.fillMaxWidth().padding(end = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Text(text = blockUnknownTitle, + modifier = Modifier + .weight(1f) + .clickable { + alertTitle.value = blockUnknownTitle + alertMessage.value = blockUnknownHelp + showAlert.value = true + }, + fontSize = 18.sp + ) + Switch( + checked = block, + onCheckedChange = { + viewModel.blockUnknown.value = it + acc.blockUnknown = it + Account.saveAccounts() + } + ) + } + } + + @Composable + fun BlockHidden(acc: Account) { + val blockHiddenTitle = stringResource(R.string.block_hidden) + val blockHiddenHelp = stringResource(R.string.block_hidden_help) + val block by viewModel.blockHidden.collectAsState() + Row( + Modifier.fillMaxWidth().padding(end = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Text(text = blockHiddenTitle, + modifier = Modifier + .weight(1f) + .clickable { + alertTitle.value = blockHiddenTitle + alertMessage.value = blockHiddenHelp + showAlert.value = true + }, + fontSize = 18.sp + ) + Switch( + checked = block, + onCheckedChange = { + viewModel.blockHidden.value = it + acc.blockHidden = it + Account.saveAccounts() + } + ) + } + } + + val showDialog = remember { mutableStateOf(false) } + val message = remember { mutableStateOf("") } + val lastAction = remember { mutableStateOf({}) } + + AlertDialog( + showDialog = showDialog, + title = stringResource(R.string.confirmation), + message = message.value, + firstButtonText = stringResource(R.string.cancel), + lastButtonText = stringResource(R.string.delete), + onLastClicked = lastAction.value, + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(contentPadding) + .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp) + ) { + BlockUnknown() + BlockHidden(acc) + Spacer(Modifier.size(16.dp)) + Text( + text = stringResource(R.string.blocking_list), + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 8.dp) + ) + if (rules.isNotEmpty()) { + val lazyListState = rememberLazyListState() + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .verticalScrollbar(state = lazyListState), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items(rules) { rule -> + val pattern = if (Utils.checkUri(rule.pattern)) + Utils.friendlyUri(ctx, rule.pattern, acc) + else + rule.pattern + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "\u2022", + modifier = Modifier.padding(start = 8.dp, end = 8.dp), + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onBackground + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = pattern, + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onBackground + ) + } + val deleteRuleMessage = stringResource(R.string.blocking_delete_alert) + IconButton( + modifier = Modifier.padding(end = 8.dp).size(32.dp), + onClick = { + message.value = String.format( + deleteRuleMessage, + pattern + ) + lastAction.value = { + BaresipService.blockRules.remove(rule) + BlockRule.save() + onRuleDeleted() + } + showDialog.value = true + } + ) { + Icon( + imageVector = Icons.Filled.Remove, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(32.dp).scale(scaleX = 1f, scaleY = 0.5f) + ) + } + } + } + } + } + else + Spacer(Modifier.weight(1f)) + } +} + +@Composable +fun NewRule(onRuleAdded: () -> Unit) { + val alertTitle = remember { mutableStateOf("") } + val alertMessage = remember { mutableStateOf("") } + val showAlert = remember { mutableStateOf(false) } + + if (showAlert.value) + AlertDialog( + showDialog = showAlert, + title = alertTitle.value, + message = alertMessage.value, + lastButtonText = stringResource(R.string.ok), + ) + + var pattern by remember { mutableStateOf("") } + val focusManager = LocalFocusManager.current + + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + val ruleHelp = stringResource(R.string.blocking_rule_help) + val newRuleTitle = stringResource(R.string.new_blocking_rule) + OutlinedTextField( + value = pattern, + placeholder = { Text(text = stringResource(R.string.new_blocking_rule)) }, + onValueChange = { pattern = it }, + modifier = Modifier + .weight(1f) + .padding(end = 8.dp) + .clickable { + alertTitle.value = newRuleTitle + alertMessage.value = ruleHelp + showAlert.value = true + }, + singleLine = true, + trailingIcon = { + if (pattern.isNotEmpty()) { + Icon( + Icons.Outlined.Clear, + contentDescription = "Clear", + modifier = Modifier.clickable { pattern = "" }, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + label = { Text(stringResource(R.string.new_blocking_rule)) }, + textStyle = TextStyle(fontSize = 18.sp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) + ) + SmallFloatingActionButton( + modifier = Modifier.offset(y = 2.dp), + onClick = { + if (pattern.trim().isNotEmpty()) { + if (!BlockRule.exists(pattern.trim())) { + BaresipService.blockRules.add(BlockRule(pattern.trim())) + BlockRule.save() + onRuleAdded() + } + pattern = "" + focusManager.clearFocus() + } + }, + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) { + Icon( + imageVector = Icons.Filled.Add, + modifier = Modifier.size(36.dp), + contentDescription = stringResource(R.string.add) + ) + } + } + } +}