diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 618a377..888e084 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -12,6 +12,7 @@ android { ndkVersion = flutter.ndkVersion compileOptions { + isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } @@ -59,3 +60,7 @@ kotlin { flutter { source = "../.." } + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b91cc55..1639dc7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,9 @@ - + + + diff --git a/assets/notification_banner.png b/assets/notification_banner.png new file mode 100644 index 0000000..73b9416 Binary files /dev/null and b/assets/notification_banner.png differ diff --git a/lib/components/notification.dart b/lib/components/notification.dart new file mode 100644 index 0000000..ad1b6b8 --- /dev/null +++ b/lib/components/notification.dart @@ -0,0 +1,69 @@ +import 'dart:io'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class VpnNotificationManager { + static final FlutterLocalNotificationsPlugin _plugin = + FlutterLocalNotificationsPlugin(); + static bool _initialized = false; + + static Future init() async { + if (_initialized) return; + + const androidSettings = + AndroidInitializationSettings('@mipmap/ic_launcher'); + const initSettings = InitializationSettings(android: androidSettings); + + await _plugin.initialize(initSettings); + + // Request POST_NOTIFICATIONS permission on Android 13+ + if (Platform.isAndroid) { + final androidPlugin = _plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); + await androidPlugin?.requestNotificationsPermission(); + } + + _initialized = true; + } + + static Future showOrUpdateStatus({ + required String timeLeft, + required String speedKbps, + }) async { + if (!_initialized) await init(); + + const androidDetails = AndroidNotificationDetails( + 'paladin_vpn_status', + 'VPN Status', + channelDescription: 'Persistent notification shown while VPN is active', + importance: Importance.low, + priority: Priority.low, + // Non-dismissable while VPN is running + ongoing: true, + autoCancel: false, + // Silent updates — no sound or vibration each time stats refresh + playSound: false, + enableVibration: false, + onlyAlertOnce: true, + // Hide the auto-generated timestamp + showWhen: false, + // Show on lock screen + visibility: NotificationVisibility.public, + ); + + const notificationDetails = NotificationDetails(android: androidDetails); + + await _plugin.show( + 888, // Fixed ID — always updates the same notification + 'Paladin VPN Active', + 'Time: $timeLeft • $speedKbps', + notificationDetails, + ); + } + + static Future cancel() async { + if (!_initialized) return; + await _plugin.cancel(888); + } +} + diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index 55a92f3..883fc20 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import 'package:paladinvpn/logic/app_config.dart'; import 'package:paladinvpn/logic/vpn_connection.dart'; import 'package:paladinvpn/logic/crypto_service.dart'; +import 'package:paladinvpn/components/notification.dart'; /// SessionTimer acts as the client-side session authority for the VPN tunnel. /// @@ -51,6 +52,16 @@ class SessionTimer extends ChangeNotifier { return '${gb.toStringAsFixed(2)} GB'; } + String get formattedDataUsed { + if (_usedBytes == 0) return '0.00 MB'; + if (_usedBytes > 1024 * 1024 * 1024) { + final gb = _usedBytes / (1024 * 1024 * 1024); + return '${gb.toStringAsFixed(2)} GB'; + } + final mb = _usedBytes / (1024 * 1024); + return '${mb.toStringAsFixed(2)} MB'; + } + double get progress { // Max duration is generally 60 or 90 minutes. We'll base the ring purely on data usage or just standard time // For UI simplicity, we'll return data progress: @@ -119,6 +130,11 @@ class SessionTimer extends ChangeNotifier { } _lastUsedBytes = _usedBytes; + VpnNotificationManager.showOrUpdateStatus( + timeLeft: formatted, + speedKbps: '${_currentSpeedKbps.toStringAsFixed(2)} KB/s', + ); + notifyListeners(); } else if (response.statusCode == 404) { // Peer not found on server (expired/deleted natively) @@ -159,6 +175,7 @@ class SessionTimer extends ChangeNotifier { _timer = null; _remainingSeconds = 0; _currentSpeedKbps = 0.0; + VpnNotificationManager.cancel(); notifyListeners(); } diff --git a/pubspec.lock b/pubspec.lock index 8e40e06..70790f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" fake_async: dependency: transitive description: @@ -134,6 +142,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + url: "https://pub.dev" + source: hosted + version: "17.2.4" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_secure_storage: dependency: "direct main" description: @@ -345,7 +377,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -392,6 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -557,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" typed_data: dependency: transitive description: @@ -716,6 +764,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d3c2148..190d1df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: url_launcher: ^6.3.2 flutter_secure_storage: ^10.3.1 uuid: ^4.5.3 + flutter_local_notifications: ^17.0.0 + path_provider: ^2.1.5 dev_dependencies: flutter_test: