android: notification settings as in the Final 2026 mockup (problems, new apps, updates)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-12 10:47:24 +07:00
1 parent 739068ccb9
commit b4a311c84f
6 files changed
+189 -7

No files matched your search

@@ -144,7 +144,13 @@ class EventLog private constructor(private val context: Context)
prune(next, now)
}
persist()
posted?.let { if (it.kind == EventKind.WARN) pushIfBackground(it) }
posted?.let {
when
{
it.kind == EventKind.WARN && NotifPrefs.enabled(context, NotifPrefs.PROBLEMS) -> pushIfBackground(it, PROBLEMS_CHANNEL)
it.kind == EventKind.ASK && NotifPrefs.enabled(context, NotifPrefs.NEW_APPS) -> pushIfBackground(it, HINTS_CHANNEL)
}
}
}
/** Records once per [key] for the whole retention time (e.g. one update notice per version). */
@@ -203,7 +209,7 @@ class EventLog private constructor(private val context: Context)
persist()
}
private fun pushIfBackground(event: ShxEvent)
private fun pushIfBackground(event: ShxEvent, channel: String)
{
val foreground = try
{
@@ -227,7 +233,7 @@ class EventLog private constructor(private val context: Context)
context, 0, Intent(context, InboxActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val notification = NotificationCompat.Builder(context, PROBLEMS_CHANNEL)
val notification = NotificationCompat.Builder(context, channel)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(event.title)
.setContentText(event.text)
@@ -300,6 +306,7 @@ class EventLog private constructor(private val context: Context)
private const val TAG = "EventLog"
private const val FILE_NAME = "events.json"
const val PROBLEMS_CHANNEL = "org.strongswan.android.problems"
const val HINTS_CHANNEL = "org.strongswan.android.hints"
const val RETENTION_MS = 30L * 24 * 60 * 60 * 1000
const val MERGE_WINDOW_MS = 30L * 60 * 1000
private const val MAX_EVENTS = 300
@@ -324,6 +331,13 @@ class EventLog private constructor(private val context: Context)
)
channel.description = context.getString(R.string.events_channel_problems_desc)
manager.createNotificationChannel(channel)
/* suggestions arrive quietly (F_NotifSettings «Quiet») */
val hints = NotificationChannel(
HINTS_CHANNEL, context.getString(R.string.events_channel_hints),
NotificationManager.IMPORTANCE_LOW,
)
hints.description = context.getString(R.string.events_channel_hints_desc)
manager.createNotificationChannel(hints)
}
}
@@ -360,3 +374,19 @@ class EventLog private constructor(private val context: Context)
}
}
}
/** Which events are also pushed (F_NotifSettings); all of them stay in «Events» anyway. */
object NotifPrefs
{
const val PROBLEMS = "pref_notif_problems"
const val NEW_APPS = "pref_notif_new_apps"
const val UPDATES = "pref_notif_updates"
@JvmStatic
fun enabled(context: Context, key: String): Boolean =
androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, true)
@JvmStatic
fun set(context: Context, key: String, value: Boolean) =
androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply()
}
@@ -453,6 +453,10 @@ class UpdateManager private constructor(private val context: Context)
/** Posts a notification that the downloaded update is ready to install */
private fun notifyUpdateReady(info: UpdateInfo)
{
if (!org.strongswan.android.logic.events.NotifPrefs.enabled(context, org.strongswan.android.logic.events.NotifPrefs.UPDATES))
{ /* switched off in the notification settings; the update stays in Settings and Events */
return
}
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
if (manager == null)
{
@@ -59,7 +59,9 @@ import org.strongswan.android.ui.compose.design.ShxIcons
import org.strongswan.android.ui.compose.design.ShxNote
import org.strongswan.android.ui.compose.design.ShxPanel
import org.strongswan.android.ui.compose.design.ShxSectionHeader
import org.strongswan.android.ui.compose.design.ShxSwitch
import org.strongswan.android.ui.compose.design.ShxToggleRow
import org.strongswan.android.logic.events.NotifPrefs
import org.strongswan.android.ui.compose.theme.LocalShxColors
import org.strongswan.android.ui.compose.theme.ShxThemeSettings
import org.strongswan.android.ui.compose.theme.ShxType
@@ -415,3 +417,110 @@ private fun HelpLink(text: String, onClick: () -> Unit)
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
}
}
private enum class NotifTag { ALWAYS, IMPORTANT, QUIET }
/**
* «Notifications» (F_NotifSettings): what is pushed besides the «Events»
* list, in three groups; the status notification cannot be turned off.
*/
@Composable
internal fun NotifSettingsPage()
{
val c = LocalShxColors.current
val context = LocalContext.current
Column(Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp))
{
Text(
stringResource(R.string.notif_intro), style = ShxType.note.copy(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Normal), color = c.textSoft,
modifier = Modifier.padding(horizontal = 4.dp),
)
NotifGroup(stringResource(R.string.notif_group_connection))
{
NotifRow(stringResource(R.string.notif_status), stringResource(R.string.notif_status_sub), NotifTag.ALWAYS, null, divider = true)
NotifRow(stringResource(R.string.notif_problems), stringResource(R.string.notif_problems_sub), NotifTag.IMPORTANT, NotifPrefs.PROBLEMS)
}
NotifGroup(stringResource(R.string.notif_group_routing))
{
NotifRow(stringResource(R.string.notif_new_apps), stringResource(R.string.notif_new_apps_sub), NotifTag.QUIET, NotifPrefs.NEW_APPS)
}
NotifGroup(stringResource(R.string.notif_group_other))
{
NotifRow(stringResource(R.string.notif_updates), stringResource(R.string.notif_updates_sub), NotifTag.QUIET, NotifPrefs.UPDATES)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.fillMaxWidth().height(52.dp).clip(RoundedCornerShape(18.dp)).background(c.tile)
.clickable(role = Role.Button) {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
else Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null))
runCatching { context.startActivity(intent) }
}
.padding(horizontal = 16.dp),
)
{
Text(stringResource(R.string.notif_android), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.textTonal, modifier = Modifier.weight(1f))
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
}
}
}
@Composable
private fun NotifGroup(title: String, content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit)
{
Column(verticalArrangement = Arrangement.spacedBy(6.dp))
{
ShxSectionHeader(title.uppercase(), Modifier.padding(start = 6.dp, end = 6.dp, top = 6.dp))
ShxPanel(content = content)
}
}
@Composable
private fun NotifRow(title: String, sub: String, tag: NotifTag, key: String?, divider: Boolean = false)
{
val c = LocalShxColors.current
val context = LocalContext.current
var on by remember { mutableStateOf(key == null || NotifPrefs.enabled(context, key)) }
val (tagBg, tagFg, tagText) = when (tag)
{
NotifTag.ALWAYS -> Triple(c.accentContainer, c.onAccentContainer, stringResource(R.string.notif_tag_always))
NotifTag.IMPORTANT -> Triple(c.warningStrong, c.onWarning, stringResource(R.string.notif_tag_important))
NotifTag.QUIET -> Triple(c.tile, c.textSoft, stringResource(R.string.notif_tag_quiet))
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth().heightIn(min = 66.dp).padding(start = 16.dp, end = 14.dp, top = 10.dp, bottom = 10.dp),
)
{
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp))
{
androidx.compose.foundation.layout.FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
itemVerticalAlignment = Alignment.CenterVertically,
)
{
Text(title, style = ShxType.rowTitle.copy(fontSize = 15.sp), color = c.text)
Text(
tagText.uppercase(), style = ShxType.tag.copy(fontSize = 10.sp), color = tagFg, maxLines = 1, softWrap = false,
modifier = Modifier.clip(RoundedCornerShape(6.dp)).background(tagBg).padding(horizontal = 7.dp, vertical = 1.dp),
)
}
Text(sub, style = ShxType.meta.copy(fontWeight = FontWeight.Normal, lineHeight = 16.sp), color = c.textSub)
}
ShxSwitch(
checked = on,
onCheckedChange = if (key == null) null else { value ->
on = value
NotifPrefs.set(context, key, value)
},
)
}
if (divider)
{
Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
}
}
@@ -404,6 +404,11 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
}
}
if (page == SettingsPage.NOTIFICATIONS)
{
NotifSettingsPage()
}
if (page == SettingsPage.HELP)
{
HelpPage(onUpdates = { pageState = SettingsPage.UPDATES })
@@ -722,6 +727,7 @@ enum class SettingsPage(val titleRes: Int)
UPDATES(R.string.settings_section_updates),
KILLSWITCH(R.string.settings_killswitch),
HELP(R.string.settings_help),
NOTIFICATIONS(R.string.settings_notifications),
}
/** One row of the settings list (F_Settings): 62dp, 36dp grey icon well, 15/700 title, 12 sub. */
@@ -782,10 +788,7 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
stringResource(R.string.settings_app) to listOf(
SettingsEntry(stringResource(R.string.settings_stability), stringResource(R.string.settings_stability_sub), ShxIcons.Phone) { onOpen(SettingsPage.STABILITY) },
SettingsEntry(stringResource(R.string.settings_notifications), stringResource(R.string.settings_notifications_sub), ShxIcons.Bell) {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
else Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, android.net.Uri.fromParts("package", context.packageName, null))
runCatching { context.startActivity(intent) }
onOpen(SettingsPage.NOTIFICATIONS)
},
SettingsEntry(
stringResource(R.string.settings_section_updates),
@@ -1057,4 +1057,22 @@
<string name="faq_a4">Стабильный надёжнее: новые адреса применяются после переподключения. Гибкий обновляет маршруты на лету, но пока в бете.</string>
<string name="faq_q5">Видит ли SHX мои сайты?</string>
<string name="faq_a5">Нет. С доступом к статистике приложение считает только объём трафика по приложениям; сайты и содержимое не сохраняются и никуда не отправляются.</string>
<string name="notif_intro">Все события всегда сохраняются в «Событиях». Здесь — что ещё приходит пушем.</string>
<string name="notif_group_connection">Подключение</string>
<string name="notif_group_routing">Маршруты</string>
<string name="notif_group_other">Прочее</string>
<string name="notif_status">Статус VPN</string>
<string name="notif_status_sub">Постоянное уведомление: сервер, время, пауза. Требует Android</string>
<string name="notif_problems">Проблемы</string>
<string name="notif_problems_sub">Белые списки, обрыв, сервер не отвечает</string>
<string name="notif_new_apps">Новые приложения</string>
<string name="notif_new_apps_sub">Предложить пустить напрямую</string>
<string name="notif_updates">Обновления приложения</string>
<string name="notif_updates_sub">Новая версия готова к установке</string>
<string name="notif_tag_always">Всегда</string>
<string name="notif_tag_important">Важно</string>
<string name="notif_tag_quiet">Тихо</string>
<string name="notif_android">Звук и вид — в настройках Android</string>
<string name="events_channel_hints">Подсказки</string>
<string name="events_channel_hints_desc">Предложения, например пустить новое приложение напрямую</string>
</resources>
@@ -1043,4 +1043,22 @@
<string name="faq_a4">Stable is more reliable: new addresses apply after a reconnect. Flexible updates routes on the fly, but is still in beta.</string>
<string name="faq_q5">Does SHX see my sites?</string>
<string name="faq_a5">No. With usage access the app only counts traffic per app; sites and content are not stored or sent anywhere.</string>
<string name="notif_intro">All events are always kept in «Events». Here is what also arrives as a push.</string>
<string name="notif_group_connection">Connection</string>
<string name="notif_group_routing">Routes</string>
<string name="notif_group_other">Other</string>
<string name="notif_status">VPN status</string>
<string name="notif_status_sub">Persistent notification: server, time, pause. Required by Android</string>
<string name="notif_problems">Problems</string>
<string name="notif_problems_sub">Whitelists, connection drops, server not responding</string>
<string name="notif_new_apps">New apps</string>
<string name="notif_new_apps_sub">Offer to let them go direct</string>
<string name="notif_updates">App updates</string>
<string name="notif_updates_sub">A new version is ready to install</string>
<string name="notif_tag_always">Always</string>
<string name="notif_tag_important">Important</string>
<string name="notif_tag_quiet">Quiet</string>
<string name="notif_android">Sound and look — in Android settings</string>
<string name="events_channel_hints">Hints</string>
<string name="events_channel_hints_desc">Suggestions such as letting a new app go direct</string>
</resources>