Skip to content

Commit 3f27f05

Browse files
authored
Merge pull request #19 from parsamrrelax/main
xray core bundled on android, cdn scanner for android, new layout, check for updates, proper icons
2 parents 07d460b + ad1e404 commit 3f27f05

24 files changed

Lines changed: 69270 additions & 120 deletions

android/app/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ android {
4040
signingConfig = signingConfigs.getByName("release")
4141
}
4242
}
43+
44+
// Ensure native libs (jniLibs) are extracted to disk so we can execute
45+
// the bundled xray binary via Process.start() on Android.
46+
packaging {
47+
jniLibs {
48+
useLegacyPackaging = true
49+
}
50+
}
4351
}
4452

4553
flutter {

android/app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
<application
77
android:label="Network Checker"
88
android:name="${applicationName}"
9-
android:icon="@mipmap/ic_launcher">
9+
android:icon="@mipmap/ic_launcher"
10+
android:extractNativeLibs="true">
1011
<activity
1112
android:name=".MainActivity"
1213
android:exported="true"
@@ -45,5 +46,10 @@
4546
<action android:name="android.intent.action.PROCESS_TEXT"/>
4647
<data android:mimeType="text/plain"/>
4748
</intent>
49+
<!-- For url_launcher: open https links in external browser -->
50+
<intent>
51+
<action android:name="android.intent.action.VIEW"/>
52+
<data android:scheme="https"/>
53+
</intent>
4854
</queries>
4955
</manifest>
34 MB
Binary file not shown.
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
package com.rdnbenet.rdnbenet
22

33
import io.flutter.embedding.android.FlutterActivity
4+
import io.flutter.embedding.engine.FlutterEngine
5+
import io.flutter.plugin.common.MethodChannel
46

5-
class MainActivity : FlutterActivity()
7+
class MainActivity : FlutterActivity() {
8+
private val CHANNEL = "com.rdnbenet.rdnbenet/native_lib"
9+
10+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
11+
super.configureFlutterEngine(flutterEngine)
12+
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
13+
if (call.method == "getNativeLibraryDir") {
14+
result.success(applicationInfo.nativeLibraryDir)
15+
} else {
16+
result.notImplemented()
17+
}
18+
}
19+
}
20+
}

assets/duckie.png

98.3 KB
Loading

assets/geoip.dat

19.3 MB
Binary file not shown.

assets/geosite.dat

Lines changed: 68363 additions & 0 deletions
Large diffs are not rendered by default.

lib/core/app_config.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// App metadata. Keep version in sync with pubspec.yaml.
2+
const String appVersion = '0.5.0';

lib/core/services/cdn_config_scanner.dart

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:io';
33

44
import 'package:flutter/foundation.dart';
5+
import 'package:flutter_curl/flutter_curl.dart' as curl;
56

67
import 'xray_binary_service.dart';
78
import 'xray_process_manager.dart';
@@ -70,7 +71,7 @@ class CdnScanConfig {
7071
this.concurrentInstances = 5,
7172
this.timeout = const Duration(seconds: 10),
7273
this.startupDelay = const Duration(seconds: 2),
73-
this.testUrl = 'https://www.gstatic.com/generate_204',
74+
this.testUrl = 'https://www.google.com/generate_204',
7475
this.basePort = 10808,
7576
this.enableXrayLogs = kDebugMode,
7677
});
@@ -100,6 +101,7 @@ class CdnConfigScanner {
100101
final CdnScanConfig config;
101102

102103
XrayProcessManager? _processManager;
104+
curl.Client? _curlClient;
103105
bool _isScanning = false;
104106
bool _shouldStop = false;
105107

@@ -194,9 +196,17 @@ class CdnConfigScanner {
194196
return true;
195197
}
196198

197-
/// Test connection through SOCKS5 proxy using curl command
198-
/// This feature is only available on desktop (Linux/Windows)
199+
/// Test connection through SOCKS5 proxy.
200+
/// Uses shell curl on desktop (Linux/Windows) and libcurl via flutter_curl on Android.
199201
Future<CdnScanResult> _testWithSocks5Proxy(String ip, int proxyPort) async {
202+
if (Platform.isAndroid) {
203+
return _testWithSocks5ProxyDart(ip, proxyPort);
204+
}
205+
return _testWithSocks5ProxyCurl(ip, proxyPort);
206+
}
207+
208+
/// Test connection through SOCKS5 proxy using curl command (Linux/Windows)
209+
Future<CdnScanResult> _testWithSocks5ProxyCurl(String ip, int proxyPort) async {
200210
try {
201211
// Use Process.run() instead of Process.start() to avoid file descriptor leaks
202212
// Process.run() automatically handles stdout/stderr cleanup
@@ -267,6 +277,68 @@ class CdnConfigScanner {
267277
}
268278
}
269279

280+
/// Test connection through SOCKS5 proxy using flutter_curl / libcurl (Android).
281+
/// Follows the exact same logic as the desktop curl command version.
282+
Future<CdnScanResult> _testWithSocks5ProxyDart(String ip, int proxyPort) async {
283+
try {
284+
assert(_curlClient != null, 'curl client must be initialized before testing');
285+
286+
final stopwatch = Stopwatch()..start();
287+
288+
final response = await _curlClient!.send(curl.Request(
289+
method: 'GET',
290+
url: config.testUrl,
291+
proxy: 'socks5h://127.0.0.1:$proxyPort',
292+
verifySSL: false,
293+
connectTimeout: config.timeout,
294+
timeout: config.timeout + const Duration(seconds: 2),
295+
)).timeout(config.timeout + const Duration(seconds: 5));
296+
297+
stopwatch.stop();
298+
final timeMs = stopwatch.elapsedMilliseconds.toDouble();
299+
300+
// Check for curl-level errors (equivalent to curl exit code != 0)
301+
if (response.errorCode != null && response.errorCode != 0) {
302+
final errorMsg = (response.errorMessage?.isNotEmpty == true)
303+
? response.errorMessage!
304+
: 'curl error code ${response.errorCode}';
305+
return CdnScanResult(
306+
ip: ip,
307+
success: false,
308+
errorMessage: errorMsg,
309+
);
310+
}
311+
312+
// Parse status code – same logic as desktop curl version
313+
final statusCode = response.statusCode;
314+
if (statusCode == 204 || (statusCode >= 200 && statusCode < 300)) {
315+
return CdnScanResult(
316+
ip: ip,
317+
success: true,
318+
latencyMs: timeMs,
319+
);
320+
} else {
321+
return CdnScanResult(
322+
ip: ip,
323+
success: false,
324+
errorMessage: 'HTTP $statusCode',
325+
);
326+
}
327+
} on TimeoutException {
328+
return CdnScanResult(
329+
ip: ip,
330+
success: false,
331+
errorMessage: 'Connection timed out',
332+
);
333+
} catch (e) {
334+
return CdnScanResult(
335+
ip: ip,
336+
success: false,
337+
errorMessage: e.toString(),
338+
);
339+
}
340+
}
341+
270342
/// Start scanning IPs
271343
Stream<CdnScanProgress> scanIps(
272344
List<String> ips,
@@ -302,6 +374,15 @@ class CdnConfigScanner {
302374
enableProcessLogs: config.enableXrayLogs,
303375
);
304376

377+
// Initialize curl client for Android (uses libcurl instead of shell curl)
378+
if (Platform.isAndroid) {
379+
_curlClient = curl.Client(
380+
verifySSL: false,
381+
verbose: kDebugMode,
382+
);
383+
await _curlClient!.init();
384+
}
385+
305386
final ipQueue = List<String>.from(ips);
306387
int ipIndex = 0;
307388
final maxConcurrency = _effectiveConcurrency();
@@ -368,7 +449,12 @@ class CdnConfigScanner {
368449
) async {
369450
XrayInstance? instance;
370451
try {
452+
if (kDebugMode) debugPrint('[CdnScan] Starting xray instance for IP=$ip');
371453
instance = await _processManager!.startInstanceForIp(baseConfig, ip);
454+
if (kDebugMode) {
455+
debugPrint('[CdnScan] Xray started for IP=$ip, port=${instance.port}, pid=${instance.process.pid}');
456+
debugPrint('[CdnScan] Waiting ${config.startupDelay.inMilliseconds}ms for xray startup...');
457+
}
372458
await Future.delayed(config.startupDelay);
373459

374460
if (_shouldStop) {
@@ -379,14 +465,40 @@ class CdnConfigScanner {
379465
);
380466
}
381467

382-
return await _testWithSocks5Proxy(ip, instance.port);
468+
// On Android, check if xray process is still alive before testing
469+
if (Platform.isAndroid) {
470+
final exitCodeFuture = instance.process.exitCode;
471+
final aliveCheck = await Future.any([
472+
exitCodeFuture.then((code) => 'exited:$code'),
473+
Future.delayed(const Duration(milliseconds: 100), () => 'alive'),
474+
]);
475+
if (kDebugMode) debugPrint('[CdnScan] Xray process status before test: $aliveCheck');
476+
if (aliveCheck.startsWith('exited:')) {
477+
final code = aliveCheck.split(':')[1];
478+
if (kDebugMode) debugPrint('[CdnScan] ERROR: xray died before proxy test! exit=$code, errors: ${instance.errorLog}');
479+
return CdnScanResult(
480+
ip: ip,
481+
success: false,
482+
errorMessage: 'Xray process died (exit=$code)',
483+
);
484+
}
485+
}
486+
487+
final result = await _testWithSocks5Proxy(ip, instance.port);
488+
if (kDebugMode) {
489+
debugPrint('[CdnScan] Test result for IP=$ip: success=${result.success}, '
490+
'latency=${result.latencyMs}ms, error=${result.errorMessage}');
491+
}
492+
return result;
383493
} on XrayStartupException catch (e) {
494+
if (kDebugMode) debugPrint('[CdnScan] XrayStartupException for IP=$ip: ${e.message}');
384495
return CdnScanResult(
385496
ip: ip,
386497
success: false,
387498
errorMessage: 'Xray start failed: ${e.message}',
388499
);
389500
} catch (e) {
501+
if (kDebugMode) debugPrint('[CdnScan] Exception for IP=$ip: $e');
390502
return CdnScanResult(
391503
ip: ip,
392504
success: false,
@@ -407,6 +519,13 @@ class CdnConfigScanner {
407519
await _processManager!.stopAll();
408520
_processManager = null;
409521
}
522+
// Dispose curl client (Android)
523+
if (_curlClient != null) {
524+
try {
525+
_curlClient!.dispose();
526+
} catch (_) {}
527+
_curlClient = null;
528+
}
410529
}
411530

412531
/// Stop the current scan
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'dart:convert';
2+
3+
import 'package:http/http.dart' as http;
4+
5+
const _releasesUrl =
6+
'https://api.github.com/repos/mirarr-app/network-checker/releases/latest';
7+
8+
const _releasesPageUrl = 'https://github.com/mirarr-app/network-checker/releases';
9+
10+
/// Fetches the latest release tag from GitHub and compares with [currentVersion].
11+
/// Returns the latest version string if it's newer, null otherwise.
12+
Future<String?> checkForUpdate(String currentVersion) async {
13+
try {
14+
final response = await http.get(Uri.parse(_releasesUrl)).timeout(
15+
const Duration(seconds: 10),
16+
);
17+
if (response.statusCode == 200) {
18+
final data = json.decode(response.body) as Map<String, dynamic>;
19+
final tagName = data['tag_name'] as String?;
20+
if (tagName != null && tagName.isNotEmpty) {
21+
final latest = _normalizeVersion(tagName);
22+
final current = _normalizeVersion(currentVersion);
23+
if (_isNewer(latest, current)) {
24+
return tagName.startsWith('v') ? tagName.substring(1) : tagName;
25+
}
26+
}
27+
}
28+
} catch (_) {
29+
// Ignore network/parse errors
30+
}
31+
return null;
32+
}
33+
34+
String get releasesPageUrl => _releasesPageUrl;
35+
36+
/// Normalize "v0.2.0" or "0.2.0" to [major, minor, patch] list.
37+
List<int> _normalizeVersion(String version) {
38+
final s = version.trim().toLowerCase().replaceFirst(RegExp(r'^v'), '');
39+
final parts = s.split(RegExp(r'[.+\-]'));
40+
return parts.take(3).map((p) => int.tryParse(p) ?? 0).toList();
41+
}
42+
43+
/// Returns true if [a] is newer than [b].
44+
bool _isNewer(List<int> a, List<int> b) {
45+
for (var i = 0; i < 3; i++) {
46+
final va = i < a.length ? a[i] : 0;
47+
final vb = i < b.length ? b[i] : 0;
48+
if (va > vb) return true;
49+
if (va < vb) return false;
50+
}
51+
return false;
52+
}

0 commit comments

Comments
 (0)