android: notification settings as in the Final 2026 mockup (problems, new apps, updates)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
739068ccb9
commit
b4a311c84f
6 files changed
+189
-7
No files matched your search
+33
-3
@@ -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()
|
||||
}
|
||||
+4
@@ -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)
|
||||
{
|
||||
|
||||
+109
@@ -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))
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -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>
|
||||
Reference in new issue
Block a user