chore: release 3.3.0 (sidebar nav fix, notification countdown, hardening)

Co-authored-by: Naveax <[email protected]>
This commit is contained in:
esefxdzandNaveax committed 2026-08-30 14:01:52 +03:00
1 parent 4eb1fca666
commit 62104d5730
28 files changed
+670 -215

No files matched your search

+24 -20
View File
@@ -1,19 +1,25 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- VpnService provides its own system notification; SPECIAL_USE is not needed -->
<!-- Required by url_launcher on Android 11+ to open external browser -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<application
<!-- flutter_vless 1.1.5 declares this legacy permission. ReVolt never needs it. -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:node="remove"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
</queries>
<application
android:label="Revolt VPN"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
@@ -21,7 +27,6 @@
android:allowBackup="false"
android:extractNativeLibs="true">
<!-- AdMob App ID — injected at build time from android/admob.properties -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="${ADMOB_APP_ID}"/>
@@ -35,17 +40,16 @@
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
android:value="2"/>
</application>
</manifest>
@@ -1,6 +1,16 @@
package com.paladinvpn.app
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.github.tfox.flutter_vless.xray.service.XrayVPNService
import com.github.tfox.flutter_vless.xray.utils.AppConfigs
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@@ -11,6 +21,11 @@ class MainActivity : FlutterActivity() {
// Must match the string in lib/logic/updater.dart.
private val installerChannel = "com.revoltvpn.app/installer"
// Must match lib/logic/notification_service.dart.
private val notificationChannel = "com.revoltvpn.app/notification"
private var notificationChannelReady = false
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -22,6 +37,102 @@ class MainActivity : FlutterActivity() {
result.notImplemented()
}
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, notificationChannel)
.setMethodCallHandler { call, result ->
if (call.method == "updateNotificationText") {
val text = call.argument<String>("text") ?: ""
val title = call.argument<String>("title") ?: "Revolt VPN"
val action = call.argument<String>("disconnectLabel") ?: "Disconnect"
updateVpnNotification(title, text, action)
result.success(null)
} else {
result.notImplemented()
}
}
}
/**
* Sets the session countdown as the VPN notification's content text.
*
* notify() is per-package, so re-posting flutter_vless's notification id
* replaces it wholesale — every field the plugin set has to be rebuilt
* here or it is dropped, the Disconnect action most of all. Keep in sync
* with XrayCoreManager.showNotification().
*/
private fun updateVpnNotification(title: String, text: String, actionLabel: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
return
}
ensureNotificationChannel()
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
// Same intent shape as the plugin, so a tap brings the running task
// forward instead of starting a second one.
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
launchIntent?.action = "FROM_DISCONNECT_BTN"
launchIntent?.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK
// Tapping "Disconnect" stops the tunnel without opening the app.
val stopIntent = Intent(this, XrayVPNService::class.java)
stopIntent.putExtra(
"COMMAND",
AppConfigs.V2RAY_SERVICE_COMMANDS.STOP_SERVICE
)
val stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, flags)
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(title)
.setContentText(text)
.addAction(0, actionLabel, stopPendingIntent)
.setColorized(true)
.setColor(0xFF0D1117.toInt())
.setOngoing(true)
.setOnlyAlertOnce(true)
.setSilent(true)
.setShowWhen(false)
.setPriority(NotificationCompat.PRIORITY_LOW)
if (launchIntent != null) {
builder.setContentIntent(
PendingIntent.getActivity(this, 0, launchIntent, flags)
)
}
try {
NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, builder.build())
} catch (_: Exception) {
// The foreground notification may not be up yet — the update is cosmetic.
}
}
/** A binder round-trip, and the caller runs once a second — so, once per process. */
private fun ensureNotificationChannel() {
if (notificationChannelReady) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Revolt VPN Background Service",
NotificationManager.IMPORTANCE_LOW
)
channel.setShowBadge(false)
manager.createNotificationChannel(channel)
}
notificationChannelReady = true
}
/**
@@ -39,4 +150,10 @@ class MainActivity : FlutterActivity() {
} catch (e: Exception) {
null
}
companion object {
// Must match flutter_vless's XrayCoreManager.
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_CHANNEL_ID = "XRAY_SERVICE_CHANNEL"
}
}