diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml
index b6a816d36..4c0b0e565 100644
--- a/src/frontends/android/app/src/main/AndroidManifest.xml
+++ b/src/frontends/android/app/src/main/AndroidManifest.xml
@@ -321,6 +321,10 @@
+
+
.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.logic.stats
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import org.strongswan.android.R
+import org.strongswan.android.logic.events.NotifPrefs
+import org.strongswan.android.ui.StatsActivity
+import org.strongswan.android.ui.compose.formatBytes
+import java.util.Calendar
+import java.util.concurrent.TimeUnit
+
+/** «Weekly summary» (F_SysNotifs): last week's traffic on Monday morning, off by default. */
+object WeeklySummary
+{
+ private const val WORK_NAME = "weekly-summary"
+ private const val CHANNEL = "org.strongswan.android.weekly"
+ private const val NOTIFICATION_ID = 0x5717
+
+ /** Arms (or cancels) the Monday 10:00 job after the switch. */
+ @JvmStatic
+ fun schedule(context: Context)
+ {
+ val wm = WorkManager.getInstance(context)
+ if (!NotifPrefs.enabled(context, NotifPrefs.WEEKLY))
+ {
+ wm.cancelUniqueWork(WORK_NAME)
+ return
+ }
+ val c = Calendar.getInstance()
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
+ c.set(Calendar.HOUR_OF_DAY, 10)
+ c.set(Calendar.MINUTE, 0)
+ c.set(Calendar.SECOND, 0)
+ if (c.timeInMillis <= System.currentTimeMillis())
+ {
+ c.add(Calendar.WEEK_OF_YEAR, 1)
+ }
+ val request = PeriodicWorkRequestBuilder(7, TimeUnit.DAYS)
+ .setInitialDelay(c.timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ .build()
+ wm.enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request)
+ }
+
+ internal fun post(context: Context)
+ {
+ if (!NotifPrefs.enabled(context, NotifPrefs.WEEKLY))
+ {
+ return
+ }
+ val day = 24L * 60 * 60 * 1000
+ val now = System.currentTimeMillis()
+ val week = TrafficHistory.range(context, now - 7 * day, now - day)
+ val total = week.sumOf { it.total }
+ if (total <= 0)
+ {
+ return
+ }
+ val vpnShare = (week.sumOf { it.vpn } * 100 / total).toInt()
+ val manager = NotificationManagerCompat.from(context)
+ if (!manager.areNotificationsEnabled())
+ {
+ return
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ {
+ context.getSystemService(NotificationManager::class.java)?.createNotificationChannel(
+ NotificationChannel(CHANNEL, context.getString(R.string.notif_weekly), NotificationManager.IMPORTANCE_LOW)
+ )
+ }
+ val open = PendingIntent.getActivity(
+ context, 0, Intent(context, StatsActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+ val notification = NotificationCompat.Builder(context, CHANNEL)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setContentTitle(context.getString(R.string.weekly_title, formatBytes(total)))
+ .setContentText(context.getString(R.string.weekly_text, vpnShare, 100 - vpnShare))
+ .setContentIntent(open)
+ .setAutoCancel(true)
+ .build()
+ try
+ {
+ manager.notify(NOTIFICATION_ID, notification)
+ }
+ catch (_: SecurityException)
+ {
+ }
+ }
+}
+
+class WeeklySummaryWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params)
+{
+ override suspend fun doWork(): Result
+ {
+ WeeklySummary.post(applicationContext)
+ return Result.success()
+ }
+}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt
index 2acefd48e..47ac73a4e 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt
@@ -481,6 +481,13 @@ class UpdateManager private constructor(private val context: Context)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setContentIntent(pending)
.setAutoCancel(true)
+ /* F_SysNotifs: «Update» / «What is new» */
+ .addAction(0, context.getString(R.string.notif_update), pending)
+ .addAction(0, context.getString(R.string.notif_whats_new), PendingIntent.getActivity(
+ context, info.versionCode.toInt() + 1,
+ Intent(context, MainActivity::class.java).putExtra(MainActivity.EXTRA_OPEN_UPDATES, true)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.build()
try
{
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java
index 6a61e219c..b776822ee 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java
@@ -58,6 +58,11 @@ public class MainActivity extends AppCompatActivity
*/
public static final String EXTRA_CONNECT_FASTEST = "org.strongswan.android.extra.CONNECT_FASTEST";
+ /**
+ * Set as an intent extra to show the Updates page (the update notification)
+ */
+ public static final String EXTRA_OPEN_UPDATES = "org.strongswan.android.extra.OPEN_UPDATES";
+
/**
* Use "bring your own device" (BYOD) features
*/
@@ -132,6 +137,11 @@ public class MainActivity extends AppCompatActivity
intent.removeExtra(EXTRA_OPEN_SERVERS);
ShxNavigation.INSTANCE.request(ShxTab.PROFILES);
}
+ if (intent != null && intent.getBooleanExtra(EXTRA_OPEN_UPDATES, false))
+ {
+ intent.removeExtra(EXTRA_OPEN_UPDATES);
+ ShxNavigation.INSTANCE.requestUpdatesPage();
+ }
if (intent != null && intent.getBooleanExtra(EXTRA_CONNECT_FASTEST, false))
{
intent.removeExtra(EXTRA_CONNECT_FASTEST);
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsPages.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsPages.kt
index 02f0f0584..4d6e3186c 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsPages.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsPages.kt
@@ -446,7 +446,8 @@ internal fun NotifSettingsPage()
}
NotifGroup(stringResource(R.string.notif_group_other))
{
- NotifRow(stringResource(R.string.notif_updates), stringResource(R.string.notif_updates_sub), NotifTag.QUIET, NotifPrefs.UPDATES)
+ NotifRow(stringResource(R.string.notif_updates), stringResource(R.string.notif_updates_sub), NotifTag.QUIET, NotifPrefs.UPDATES, divider = true)
+ NotifRow(stringResource(R.string.notif_weekly), stringResource(R.string.notif_weekly_sub), NotifTag.QUIET, NotifPrefs.WEEKLY)
}
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -516,6 +517,7 @@ private fun NotifRow(title: String, sub: String, tag: NotifTag, key: String?, di
onCheckedChange = if (key == null) null else { value ->
on = value
NotifPrefs.set(context, key, value)
+ if (key == NotifPrefs.WEEKLY) org.strongswan.android.logic.stats.WeeklySummary.schedule(context)
},
)
}
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt
index 04fef9804..d74ff61a3 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/SettingsScreen.kt
@@ -213,6 +213,15 @@ fun SettingsScreen(profiles: List, showTitle: Boolean = true)
}
var pageState by rememberSaveable { mutableStateOf(SettingsPage.ROOT) }
+ val updatesRequested by ShxNavigation.updatesPage.collectAsStateWithLifecycle()
+ androidx.compose.runtime.LaunchedEffect(updatesRequested)
+ {
+ if (updatesRequested)
+ {
+ pageState = SettingsPage.UPDATES
+ ShxNavigation.consumeUpdatesPage()
+ }
+ }
/* from a sub page "back" returns to the settings list, not out of the tab */
val effects = MaterialTheme.motionScheme.defaultEffectsSpec()
val spatial = MaterialTheme.motionScheme.defaultSpatialSpec()
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt
index e8ac5306d..f8c4b208f 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/ShxNavigation.kt
@@ -63,4 +63,19 @@ object ShxNavigation
{
_fastest.value = false
}
+
+ /* «What is new» of the update notification: the Updates page of Settings */
+ private val _updatesPage = MutableStateFlow(false)
+ val updatesPage: StateFlow = _updatesPage
+
+ fun requestUpdatesPage()
+ {
+ request(ShxTab.SETTINGS)
+ _updatesPage.value = true
+ }
+
+ fun consumeUpdatesPage()
+ {
+ _updatesPage.value = false
+ }
}
diff --git a/src/frontends/android/app/src/main/res/values-ru/strings.xml b/src/frontends/android/app/src/main/res/values-ru/strings.xml
index 4f9f67ed6..0f2cf9ef6 100644
--- a/src/frontends/android/app/src/main/res/values-ru/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-ru/strings.xml
@@ -1318,4 +1318,16 @@
реклама
трекеры
фишинг
+ Подключаемся к %1$s…
+ Сеть, сервер, вход, туннель
+ Стоп
+ Да
+ Нет
+ Подробнее
+ Обновить
+ Что нового
+ Итоги недели
+ Трафик за неделю, по понедельникам
+ За неделю: %1$s
+ %1$d%% через VPN · %2$d%% напрямую
diff --git a/src/frontends/android/app/src/main/res/values/strings.xml b/src/frontends/android/app/src/main/res/values/strings.xml
index b94a63c7b..2f78d825e 100644
--- a/src/frontends/android/app/src/main/res/values/strings.xml
+++ b/src/frontends/android/app/src/main/res/values/strings.xml
@@ -1304,4 +1304,16 @@
ads
trackers
phishing
+ Connecting to %1$s…
+ Network, server, sign-in, tunnel
+ Stop
+ Yes
+ No
+ Details
+ Update
+ What is new
+ Weekly summary
+ Traffic of the week, on Mondays
+ Last week: %1$s
+ %1$d%% through the VPN · %2$d%% direct