style: cut the comments down
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
66789d49c1
commit
30d96a4049
11 files changed
+59
-132
No files matched your search
@@ -70,10 +70,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
updateVpnNotification(title, text, action)
|
updateVpnNotification(title, text, action)
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
// Session ends only Dart can see (cap exhausted, server
|
// Session ends only Dart sees; the service posts the rest.
|
||||||
// ended the session, VPN error). The VPN service posts the
|
|
||||||
// same alert for its own deadline, where this process may
|
|
||||||
// already be gone. RevoltAlerts owns both.
|
|
||||||
"showDisconnectedAlert" -> {
|
"showDisconnectedAlert" -> {
|
||||||
RevoltAlerts.postDisconnected(this)
|
RevoltAlerts.postDisconnected(this)
|
||||||
result.success(null)
|
result.success(null)
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import 'package:revoltvpn/logic/app_colors.dart';
|
|||||||
import 'package:revoltvpn/logic/session_timer.dart';
|
import 'package:revoltvpn/logic/session_timer.dart';
|
||||||
import 'package:revoltvpn/logic/vpn_connection.dart';
|
import 'package:revoltvpn/logic/vpn_connection.dart';
|
||||||
|
|
||||||
// Nothing rendered the connection's own messages, so they are collected here
|
// The one line worth showing right now, and whether it is a problem.
|
||||||
// in one place: whatever the user most needs to know right now, and whether it
|
|
||||||
// is a problem (red) or just progress (dim).
|
|
||||||
class _StatusDetail {
|
class _StatusDetail {
|
||||||
const _StatusDetail(this.text, {this.isProblem = false});
|
const _StatusDetail(this.text, {this.isProblem = false});
|
||||||
|
|
||||||
@@ -26,8 +24,7 @@ _StatusDetail? _detail(VpnConnection vpn, SessionTimer timer) {
|
|||||||
return _StatusDetail(vpn.statusMessage);
|
return _StatusDetail(vpn.statusMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tunnel still carries traffic; only the control plane is missing, so
|
// Control plane only - the tunnel still carries traffic.
|
||||||
// this is a warning, not a disconnect (review #2).
|
|
||||||
if (vpn.status == VpnStatus.connected && timer.serverUnreachable) {
|
if (vpn.status == VpnStatus.connected && timer.serverUnreachable) {
|
||||||
return const _StatusDetail(
|
return const _StatusDetail(
|
||||||
'Can\'t reach the server — still connected',
|
'Can\'t reach the server — still connected',
|
||||||
@@ -136,9 +133,7 @@ class StatusBar extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Until this existed, a failed connect, an expired session and a
|
// Errors and session-end reasons used to render nowhere.
|
||||||
// cap kill all just turned the button back to idle - the user
|
|
||||||
// could not tell protected from unprotected (review #3).
|
|
||||||
if (detail != null)
|
if (detail != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
|
|||||||
@@ -96,18 +96,14 @@ class AdManager extends ChangeNotifier {
|
|||||||
|
|
||||||
// ── Show ad (or debug bypass) ─────────────────────────────────────
|
// ── Show ad (or debug bypass) ─────────────────────────────────────
|
||||||
|
|
||||||
// Returns the nonce carried in the SSV custom_data when the reward was
|
// Returns the nonce put in the SSV custom_data, null if unrewarded. connect()
|
||||||
// earned, null otherwise. The main flow passes it to vpn.connect(adNonce:)
|
// must reuse it: minting a second nonce there rejects the ad's own session.
|
||||||
// so the client waits for the session this ad created — minting a second
|
|
||||||
// nonce there is what broke every connect once ads were on.
|
|
||||||
Future<String?> showAd(String adType) async {
|
Future<String?> showAd(String adType) async {
|
||||||
if (!adsEnabled) {
|
if (!adsEnabled) {
|
||||||
// Main connect: no ad to show — HivemindService.fetchConfigDirectly()
|
// Main connect fires its own bypass in fetchConfigDirectly (rule 87).
|
||||||
// fires the server-gated bypass itself (rule 87).
|
|
||||||
if (adType != 'support' || !kDebugMode) return null;
|
if (adType != 'support' || !kDebugMode) return null;
|
||||||
|
|
||||||
// Debug bypass: fire fake AdMob callback so support ads work in dev.
|
// Needs ADMOB_BYPASS=True server-side.
|
||||||
// The server's ADMOB_BYPASS must be True for this to succeed.
|
|
||||||
final deviceId = await CryptoService.getDeviceId();
|
final deviceId = await CryptoService.getDeviceId();
|
||||||
final nonce = HivemindService.newNonce();
|
final nonce = HivemindService.newNonce();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -81,15 +81,10 @@ class HivemindService {
|
|||||||
_expectedNonce = null;
|
_expectedNonce = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches this device's VLESS URL. Three ways in:
|
// Fetches this device's VLESS URL.
|
||||||
// adNonce != null — a real rewarded ad was watched; its SSV custom_data
|
// adNonce — wait for the session that ad created (SSV can lag, so poll longer)
|
||||||
// carried this nonce, so wait for a session holding it.
|
// resume — re-attach to the session already held; any nonce accepted
|
||||||
// Google's SSV callback can lag, so poll longer.
|
// neither — ads off: mint a nonce and fire the bypass (rule 87)
|
||||||
// resume == true — re-attach to the session already granted (mode-switch
|
|
||||||
// reconnect in connection_mode.dart). No ad, no bypass,
|
|
||||||
// any nonce accepted — device_id already owns it.
|
|
||||||
// neither — ads disabled: mint a nonce and fire the server-gated
|
|
||||||
// ADMOB_BYPASS callback (rule 87).
|
|
||||||
static Future<String> fetchConfigDirectly({
|
static Future<String> fetchConfigDirectly({
|
||||||
void Function(int attempt, int total)? onAttempt,
|
void Function(int attempt, int total)? onAttempt,
|
||||||
String? adNonce,
|
String? adNonce,
|
||||||
|
|||||||
@@ -32,15 +32,12 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Posts the "disconnected - not protected" alert for a session end the user
|
// Same id as the service's own alert, so a double post replaces.
|
||||||
// did not ask for. The native service posts the same alert (same id) when
|
|
||||||
// its own deadline fires, because by then this process may be gone; a double
|
|
||||||
// post replaces rather than stacks.
|
|
||||||
static Future<void> showDisconnectedAlert() async {
|
static Future<void> showDisconnectedAlert() async {
|
||||||
try {
|
try {
|
||||||
await _channel.invokeMethod<void>('showDisconnectedAlert');
|
await _channel.invokeMethod<void>('showDisconnectedAlert');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Informational only - never worth failing a disconnect over.
|
// Informational only.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
|
|
||||||
int _lastUsedBytes = 0;
|
int _lastUsedBytes = 0;
|
||||||
Stopwatch? _sinceLastSuccessfulSync;
|
Stopwatch? _sinceLastSuccessfulSync;
|
||||||
// Wall-clock deadline, kept alongside the Stopwatch because they answer
|
// Wall clock, because the Stopwatch is CLOCK_MONOTONIC and stops across
|
||||||
// different questions. The Stopwatch is CLOCK_MONOTONIC: it does not advance
|
// device suspend - exactly when the RTC_WAKEUP expiry alarm fires.
|
||||||
// while the device is suspended, which is exactly when an RTC_WAKEUP expiry
|
|
||||||
// alarm fires, so it cannot be used to recognise an expiry after sleep.
|
|
||||||
DateTime? _deadlineAt;
|
DateTime? _deadlineAt;
|
||||||
double _currentSpeedKBps = 0.0;
|
double _currentSpeedKBps = 0.0;
|
||||||
|
|
||||||
@@ -70,14 +68,10 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
int get usedBytes => _usedBytes;
|
int get usedBytes => _usedBytes;
|
||||||
double get currentSpeedKBps => _currentSpeedKBps;
|
double get currentSpeedKBps => _currentSpeedKBps;
|
||||||
|
|
||||||
// True once the control plane has missed enough polls in a row to be worth
|
// Display state only - an unreachable API never tears down a live tunnel.
|
||||||
// telling the user about. It is a display state only: a tunnel that still
|
|
||||||
// carries traffic is never torn down because the API is unreachable (the
|
|
||||||
// native deadline and the server both still enforce expiry).
|
|
||||||
bool get serverUnreachable => _consecutiveFailures >= _maxConsecutiveFailures;
|
bool get serverUnreachable => _consecutiveFailures >= _maxConsecutiveFailures;
|
||||||
|
|
||||||
// Why the last session ended, when the user did not end it themselves.
|
// Why the last session ended, if the user did not end it. Cleared on connect.
|
||||||
// Cleared on the next connect. Rendered by status_bar.dart.
|
|
||||||
String? _lastEndMessage;
|
String? _lastEndMessage;
|
||||||
String? get lastEndMessage => _lastEndMessage;
|
String? get lastEndMessage => _lastEndMessage;
|
||||||
|
|
||||||
@@ -146,19 +140,15 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (vpnConnection.status == VpnStatus.disconnected) {
|
if (vpnConnection.status == VpnStatus.disconnected) {
|
||||||
// The native deadline fires on the exact second while this 1 s tick is
|
// The native deadline beats the 1 s tick, so expiry lands here, not in
|
||||||
// in arbitrary phase, so expiry almost always arrives here rather than
|
// _tick. postAlert is false: the service already posted, and a
|
||||||
// at the countdown check in _tick - without this, the commonest session
|
// notification-Disconnect tap is the user asking.
|
||||||
// end left the UI with nothing to say.
|
|
||||||
final deadline = _deadlineAt;
|
final deadline = _deadlineAt;
|
||||||
final expired = _hasSyncedOnce &&
|
final expired = _hasSyncedOnce &&
|
||||||
deadline != null &&
|
deadline != null &&
|
||||||
DateTime.now().isAfter(
|
DateTime.now().isAfter(
|
||||||
deadline.subtract(const Duration(seconds: 5)),
|
deadline.subtract(const Duration(seconds: 5)),
|
||||||
);
|
);
|
||||||
// postAlert is false either way: the native side already posted for its
|
|
||||||
// own deadline, and the notification's Disconnect action is the user
|
|
||||||
// asking, which must never raise a "not protected" alert.
|
|
||||||
unawaited(_doDisconnect(
|
unawaited(_doDisconnect(
|
||||||
'VPN tunnel dropped',
|
'VPN tunnel dropped',
|
||||||
endedMessage:
|
endedMessage:
|
||||||
@@ -168,9 +158,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetSupportClaim is false when re-attaching to a session the device
|
// resetSupportClaim is false when re-attaching to a session already held:
|
||||||
// already holds: the support top-up is once per server session, and the
|
// the top-up is once per server session and this flag is what enforces it.
|
||||||
// claim flag is the only thing enforcing that.
|
|
||||||
Future<void> start({bool resetSupportClaim = true}) async {
|
Future<void> start({bool resetSupportClaim = true}) async {
|
||||||
_sessionEpoch++;
|
_sessionEpoch++;
|
||||||
_remainingSeconds = 0;
|
_remainingSeconds = 0;
|
||||||
@@ -255,11 +244,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
await _doDisconnect(reason);
|
await _doDisconnect(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
// endedMessage is the one sentence the UI shows after an end the user did
|
// endedMessage is what the UI shows; postAlert raises the notification.
|
||||||
// not ask for; postAlert independently decides whether to raise the "not
|
// A user-requested disconnect passes neither.
|
||||||
// protected" notification, which the native service already posts for every
|
|
||||||
// stop it sees. Both exist so nobody is left on their real IP without being
|
|
||||||
// told. A user-requested disconnect passes neither.
|
|
||||||
Future<void> _doDisconnect(
|
Future<void> _doDisconnect(
|
||||||
String reason, {
|
String reason, {
|
||||||
String? endedMessage,
|
String? endedMessage,
|
||||||
@@ -428,12 +414,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
_remainingSeconds =
|
_remainingSeconds =
|
||||||
(_remainingAtLastSync - elapsed).clamp(0, 1 << 31).toInt();
|
(_remainingAtLastSync - elapsed).clamp(0, 1 << 31).toInt();
|
||||||
|
|
||||||
// The Stopwatch is CLOCK_MONOTONIC and stops while the device is
|
// The Stopwatch over-reports after device suspend. Take the smaller of
|
||||||
// suspended, so after a long sleep it over-reports - and with the server
|
// the two, never the wall clock alone - a backward clock must not extend.
|
||||||
// unreachable no sync arrives to correct it, which is exactly the state
|
|
||||||
// the app now stays in rather than disconnecting. Take whichever is
|
|
||||||
// smaller, never the wall clock alone: a clock set backwards must not be
|
|
||||||
// able to extend a session.
|
|
||||||
final deadline = _deadlineAt;
|
final deadline = _deadlineAt;
|
||||||
if (deadline != null) {
|
if (deadline != null) {
|
||||||
final byWallClock = deadline.difference(DateTime.now()).inSeconds;
|
final byWallClock = deadline.difference(DateTime.now()).inSeconds;
|
||||||
|
|||||||
@@ -224,9 +224,8 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SOCKS5 mode skips requestPermission below (it would raise the VPN
|
// Proxy mode skips requestPermission below (VPN consent it never needs),
|
||||||
// consent dialog for an interface it never creates), so ask for the
|
// so it has to ask for POST_NOTIFICATIONS on its own.
|
||||||
// notification permission on its own - the session alerts need it.
|
|
||||||
if (!kIsWeb && _activeMode == ConnectionMode.proxy) {
|
if (!kIsWeb && _activeMode == ConnectionMode.proxy) {
|
||||||
try {
|
try {
|
||||||
await _nativeControl.invokeMethod<void>('requestNotificationPermission');
|
await _nativeControl.invokeMethod<void>('requestNotificationPermission');
|
||||||
|
|||||||
@@ -67,10 +67,8 @@ class _ConnectionModeTileState extends State<ConnectionModeTile> {
|
|||||||
widget.onChanged?.call(next);
|
widget.onChanged?.call(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recovers from an orphaned proxy: tear the runtime down and re-attach to
|
// Recovers an orphaned proxy: tear the runtime down and re-attach to the
|
||||||
// the session this device already holds (connect(resume: true)), so a
|
// session already held. No ad, no new session.
|
||||||
// readable set of credentials exists again. No rewarded ad and no new
|
|
||||||
// session — the user is repairing a connection they already paid for.
|
|
||||||
Future<void> _reconnect() async {
|
Future<void> _reconnect() async {
|
||||||
if (_reconnecting) return;
|
if (_reconnecting) return;
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = context.read<VpnConnection>();
|
||||||
|
|||||||
+1
-4
@@ -240,10 +240,7 @@ class FlutterVlessPlugin : FlutterPlugin, ActivityAware, PluginRegistry.Activity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST_NOTIFICATIONS on its own, without VpnService.prepare: SOCKS5
|
// POST_NOTIFICATIONS without VpnService.prepare, for proxy mode.
|
||||||
// mode never establishes a VPN interface, so it never called
|
|
||||||
// requestPermission and both session alerts were dropped unasked
|
|
||||||
// for on Android 13+.
|
|
||||||
"requestNotificationPermission" -> {
|
"requestNotificationPermission" -> {
|
||||||
val currentActivity = activity
|
val currentActivity = activity
|
||||||
if (currentActivity == null) {
|
if (currentActivity == null) {
|
||||||
|
|||||||
+21
-40
@@ -67,9 +67,8 @@ class XrayVPNService : VpnService() {
|
|||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must stay above the command handling below: an intent carrying no
|
// Must stay above the command dispatch: an intent with no COMMAND
|
||||||
// COMMAND falls through to the final else and stops a live tunnel - in
|
// falls through to the final else and stops the tunnel.
|
||||||
// this case five minutes early, which is what it warns about.
|
|
||||||
if (intent.getBooleanExtra(EXTRA_SESSION_WARNING, false)) {
|
if (intent.getBooleanExtra(EXTRA_SESSION_WARNING, false)) {
|
||||||
handleSessionWarningAlarm(intent.getStringExtra("RUNTIME_TOKEN").orEmpty())
|
handleSessionWarningAlarm(intent.getStringExtra("RUNTIME_TOKEN").orEmpty())
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
@@ -428,12 +427,8 @@ class XrayVPNService : VpnService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// userRequested marks the two stops the user drove themselves. Everything
|
// Anything that stops a live tunnel unasked must say so. isRunning is read
|
||||||
// else that reaches here while a tunnel is live - the expiry deadline, an
|
// before cleanup() clears it, which keeps a failed connect silent.
|
||||||
// Xray or TUN failure, a fail-closed deadline arm - took protection away
|
|
||||||
// without being asked, and must say so. isRunning is read before cleanup()
|
|
||||||
// clears it, which is also what keeps a failed connect silent: it never had
|
|
||||||
// a live tunnel to interrupt.
|
|
||||||
private fun stopAll(
|
private fun stopAll(
|
||||||
confirmationToken: String = currentConfig?.RUNTIME_TOKEN.orEmpty(),
|
confirmationToken: String = currentConfig?.RUNTIME_TOKEN.orEmpty(),
|
||||||
userRequested: Boolean = false,
|
userRequested: Boolean = false,
|
||||||
@@ -479,33 +474,26 @@ class XrayVPNService : VpnService() {
|
|||||||
else deadlineHandler.postDelayed(sessionDeadlineRunnable, delayMs)
|
else deadlineHandler.postDelayed(sessionDeadlineRunnable, delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedules the five-minute warning off the same deadline the expiry alarm
|
// Advisory, unlike the deadline itself: nothing here fails closed. Dart
|
||||||
// holds. Unlike the deadline this is advisory: a warning that cannot be
|
// re-pushes the deadline every sync, so this runs about once a minute.
|
||||||
// scheduled must never stop a working tunnel, so nothing here fails closed.
|
|
||||||
// Dart re-pushes the deadline on every sync, so this runs about once a minute.
|
|
||||||
private fun armExpiryWarning(runtimeToken: String, expiresAtEpochMs: Long) {
|
private fun armExpiryWarning(runtimeToken: String, expiresAtEpochMs: Long) {
|
||||||
if (runtimeToken.isEmpty()) return
|
if (runtimeToken.isEmpty()) return
|
||||||
|
|
||||||
val warnAt = expiresAtEpochMs - RevoltAlerts.WARNING_LEAD_MS
|
val warnAt = expiresAtEpochMs - RevoltAlerts.WARNING_LEAD_MS
|
||||||
// Deadline already inside the warning window (or past): either the alarm
|
// Inside the window already (or the short bootstrap deadline). Cancel
|
||||||
// has fired and its notification should stay up, or this is the short
|
// nothing here: with no dedupe on the re-push, cancelling on this
|
||||||
// bootstrap deadline at connect, which must not warn at all. Nothing is
|
// branch deletes the armed warning and never re-arms it.
|
||||||
// cancelled here - Dart re-pushes the same deadline every 60 s with no
|
|
||||||
// dedupe, so cancelling on this branch would reliably delete the armed
|
|
||||||
// warning on the first sync after it came due and never re-arm it.
|
|
||||||
if (warnAt <= System.currentTimeMillis() + WARNING_REARM_GRACE_MS) return
|
if (warnAt <= System.currentTimeMillis() + WARNING_REARM_GRACE_MS) return
|
||||||
|
|
||||||
cancelSessionWarningAlarm()
|
cancelSessionWarningAlarm()
|
||||||
// More than the lead away, so any warning on screen belongs to an older
|
// A warning on screen now belongs to an older deadline (top-up).
|
||||||
// deadline - a support top-up just pushed it out.
|
|
||||||
RevoltAlerts.cancelExpiryWarning(this)
|
RevoltAlerts.cancelExpiryWarning(this)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
val pendingIntent = sessionWarningPendingIntent(runtimeToken)
|
val pendingIntent = sessionWarningPendingIntent(runtimeToken)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Inexact on purpose: minute-level slop is fine for a warning,
|
// Inexact on purpose - keeps this off SCHEDULE_EXACT_ALARM.
|
||||||
// and it keeps this off the SCHEDULE_EXACT_ALARM path.
|
|
||||||
alarmManager.setAndAllowWhileIdle(
|
alarmManager.setAndAllowWhileIdle(
|
||||||
AlarmManager.RTC_WAKEUP,
|
AlarmManager.RTC_WAKEUP,
|
||||||
warnAt,
|
warnAt,
|
||||||
@@ -522,8 +510,7 @@ class XrayVPNService : VpnService() {
|
|||||||
private fun handleSessionWarningAlarm(alarmToken: String) {
|
private fun handleSessionWarningAlarm(alarmToken: String) {
|
||||||
val config = currentConfig
|
val config = currentConfig
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
// The alarm woke a service process with no live runtime; there is
|
// Woke a process with no live runtime; nothing to warn about.
|
||||||
// no session left to warn about.
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -537,9 +524,8 @@ class XrayVPNService : VpnService() {
|
|||||||
if (remaining < WARNING_FLOOR_MS ||
|
if (remaining < WARNING_FLOOR_MS ||
|
||||||
remaining > RevoltAlerts.WARNING_LEAD_MS + WARNING_SLACK_MS
|
remaining > RevoltAlerts.WARNING_LEAD_MS + WARNING_SLACK_MS
|
||||||
) {
|
) {
|
||||||
// Too late to be a five-minute warning, or for a deadline that has
|
// Too late, or the deadline moved. Saying "5 minutes" with
|
||||||
// moved. Saying "5 minutes" with seconds left is worse than silence;
|
// seconds left is worse than silence.
|
||||||
// the disconnected alert follows either way.
|
|
||||||
Log.w(TAG, "Ignoring session warning outside the warning window")
|
Log.w(TAG, "Ignoring session warning outside the warning window")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -707,10 +693,8 @@ class XrayVPNService : VpnService() {
|
|||||||
private fun cancelSessionWarningAlarm() {
|
private fun cancelSessionWarningAlarm() {
|
||||||
try {
|
try {
|
||||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
// Look the existing PendingIntent up instead of building one: with
|
// Look it up rather than build it: FLAG_UPDATE_CURRENT would
|
||||||
// FLAG_UPDATE_CURRENT, constructing it to cancel rewrites the live
|
// rewrite the live alarm's RUNTIME_TOKEN to "" before cancelling.
|
||||||
// alarm's RUNTIME_TOKEN to "" first, and a delivery already in
|
|
||||||
// flight would then be dropped as a stale generation.
|
|
||||||
existingSessionWarningPendingIntent()?.let { alarmManager.cancel(it) }
|
existingSessionWarningPendingIntent()?.let { alarmManager.cancel(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to cancel session-warning alarm", e)
|
Log.w(TAG, "Failed to cancel session-warning alarm", e)
|
||||||
@@ -725,9 +709,8 @@ class XrayVPNService : VpnService() {
|
|||||||
return PendingIntent.getService(this, SESSION_WARNING_REQUEST_CODE, intent, flags)
|
return PendingIntent.getService(this, SESSION_WARNING_REQUEST_CODE, intent, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A PendingIntent is identified by component plus request code, not extras,
|
// Identity is component + request code, not extras, so the warning needs
|
||||||
// so the warning needs its own code or it would overwrite - and later
|
// its own code or it clobbers the expiry alarm.
|
||||||
// cancel - the expiry alarm.
|
|
||||||
private fun sessionWarningPendingIntent(runtimeToken: String): PendingIntent {
|
private fun sessionWarningPendingIntent(runtimeToken: String): PendingIntent {
|
||||||
val intent = Intent(this, XrayVPNService::class.java)
|
val intent = Intent(this, XrayVPNService::class.java)
|
||||||
.putExtra(EXTRA_SESSION_WARNING, true)
|
.putExtra(EXTRA_SESSION_WARNING, true)
|
||||||
@@ -814,13 +797,11 @@ class XrayVPNService : VpnService() {
|
|||||||
private const val EXTRA_SESSION_WARNING = "SESSION_WARNING"
|
private const val EXTRA_SESSION_WARNING = "SESSION_WARNING"
|
||||||
private const val SESSION_EXPIRY_REQUEST_CODE = 1001
|
private const val SESSION_EXPIRY_REQUEST_CODE = 1001
|
||||||
private const val SESSION_WARNING_REQUEST_CODE = 1002
|
private const val SESSION_WARNING_REQUEST_CODE = 1002
|
||||||
// An idle-capable alarm can run late; accept that much drift before
|
// Drift allowed before a warning is treated as belonging elsewhere.
|
||||||
// treating a warning as belonging to a deadline that has moved.
|
|
||||||
private const val WARNING_SLACK_MS = 60_000L
|
private const val WARNING_SLACK_MS = 60_000L
|
||||||
// Don't re-arm (and so re-post) for a deadline that only moved by the
|
// Don't re-arm for a deadline that moved by round-trip jitter alone.
|
||||||
// jitter in a network round trip.
|
|
||||||
private const val WARNING_REARM_GRACE_MS = 15_000L
|
private const val WARNING_REARM_GRACE_MS = 15_000L
|
||||||
// Below this the warning has lost its point; stay quiet instead.
|
// Below this the warning has lost its point.
|
||||||
private const val WARNING_FLOOR_MS = 60_000L
|
private const val WARNING_FLOOR_MS = 60_000L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-16
@@ -12,23 +12,16 @@ import android.util.Log
|
|||||||
|
|
||||||
// local_packages/.../xray/utils/RevoltAlerts.kt
|
// local_packages/.../xray/utils/RevoltAlerts.kt
|
||||||
//
|
//
|
||||||
// The two user-facing session alerts, built in one place because two processes
|
// The two session alerts. Both XrayVPNService (:RunSoLibXrayDaemon) and
|
||||||
// post them: XrayVPNService (:RunSoLibXrayDaemon) owns the native deadline and
|
// MainActivity post them, sharing ids so a second post replaces. Separate from
|
||||||
// can still warn when the UI process is gone, while MainActivity posts the
|
// the foreground-service notification (id 1, IMPORTANCE_LOW): these must be
|
||||||
// "not protected" alert for session ends only Dart can see (cap exhausted,
|
// noticed, and must be dismissible.
|
||||||
// server ended the session, VPN error). Both use the same ids, so whichever
|
|
||||||
// posts second just replaces the first notification instead of stacking.
|
|
||||||
//
|
|
||||||
// These are deliberately NOT the foreground-service notification (id 1,
|
|
||||||
// channel REVOLT_VPN_SERVICE, IMPORTANCE_LOW): an alert the user must notice
|
|
||||||
// needs its own high-importance channel, and must be dismissible.
|
|
||||||
object RevoltAlerts {
|
object RevoltAlerts {
|
||||||
|
|
||||||
const val CHANNEL_ID = "REVOLT_VPN_ALERTS"
|
const val CHANNEL_ID = "REVOLT_VPN_ALERTS"
|
||||||
const val WARNING_NOTIFICATION_ID = 2
|
const val WARNING_NOTIFICATION_ID = 2
|
||||||
const val DISCONNECTED_NOTIFICATION_ID = 3
|
const val DISCONNECTED_NOTIFICATION_ID = 3
|
||||||
|
|
||||||
// How long before the session deadline the warning fires.
|
|
||||||
const val WARNING_LEAD_MS = 5 * 60 * 1000L
|
const val WARNING_LEAD_MS = 5 * 60 * 1000L
|
||||||
|
|
||||||
private const val TAG = "RevoltAlerts"
|
private const val TAG = "RevoltAlerts"
|
||||||
@@ -97,8 +90,7 @@ object RevoltAlerts {
|
|||||||
.setSmallIcon(icon)
|
.setSmallIcon(icon)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
// Both processes can post the disconnected alert; the second
|
// A replacing post must not sound again.
|
||||||
// post replaces the first and must not sound again.
|
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
|
|
||||||
launchIntent(context)?.let { builder.setContentIntent(it) }
|
launchIntent(context)?.let { builder.setContentIntent(it) }
|
||||||
@@ -118,9 +110,7 @@ object RevoltAlerts {
|
|||||||
private fun manager(context: Context): NotificationManager? =
|
private fun manager(context: Context): NotificationManager? =
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
||||||
|
|
||||||
// Importance is fixed when the channel is first created, so this asks for
|
// Importance is fixed at creation, so ask for HIGH up front.
|
||||||
// HIGH up front — a session alert the user never sees is the whole bug
|
|
||||||
// these notifications exist to fix.
|
|
||||||
private fun ensureChannel(manager: NotificationManager) {
|
private fun ensureChannel(manager: NotificationManager) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
||||||
|
|||||||
Reference in new issue
Block a user