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