Refactor VPN setup and V2Ray initialization logic

Simplified VPN configuration, enhanced permission handling, and improved V2Ray file checks to ensure smoother setup. Updated assets and JSON configs to align with new network requirements and reduced redundancy in logging and error handling.
This commit is contained in:
yjj38 2025-05-25 23:22:52 +08:00
parent 9e64a65d62
commit b46404bb1e
16 changed files with 24570 additions and 88 deletions

View File

@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-23T10:42:56.974007600Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=89NX0C56X" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

View File

@ -3,19 +3,30 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permision.VPN_SERVICE" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/box"
android:label="Script helper"
@ -32,6 +43,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".proxy.CustomVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -0,0 +1,123 @@
{
"log": {
"loglevel": "trace"
},
"dns": {
"servers": [
{
"tag": "cloudflare",
"address": "tls://1.1.1.1"
},
{
"tag": "local",
"address": "tls://1.1.1.1",
"detour": "direct"
},
{
"tag": "remote",
"address": "8.8.8.8"
}
],
"rules": [
{
"server": "local",
"outbound": "any"
},
{
"server": "remote",
"query_type": ["A", "AAAA"]
}
],
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/16",
"inet6_range": "fd00::/8"
}
},
"inbounds": [
{
"protocol": "tun",
"tag": "tun-in",
"settings": {
"autoRoute": true,
"domainStrategy": "ipv4_only",
"sniffingEnabled": true,
"strictRoute": false
},
"streamSettings": {
"network": "tcp"
},
"address": ["172.19.0.1/28"]
}
],
"outbounds": [
{
"protocol": "socks",
"tag": "socks-out",
"settings": {
"servers": [
{
"address": "105bd58a50330382.na.ipidea.online",
"port": 2333,
"users": [
{
"user": "cut_team_protoc_vast-zone-custom-region-us",
"pass": "Leoliu811001"
}
]
}
]
},
"streamSettings": {
"network": "tcp"
},
"udpSettings": {
"enabled": true
}
},
{
"protocol": "dns",
"tag": "dns-out"
},
{
"protocol": "freedom",
"tag": "direct",
"settings": {}
},
{
"protocol": "blackhole",
"tag": "block",
"settings": {}
}
],
"routing": {
"domainStrategy": "IPOnDemand",
"rules": [
{
"type": "field",
"protocol": "dns",
"outboundTag": "dns-out"
},
{
"type": "field",
"protocol": ["stun", "quic"],
"outboundTag": "block"
},
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "direct"
},
{
"type": "field",
"ip": ["8.217.74.194/32"],
"outboundTag": "direct"
},
{
"type": "field",
"domain": ["cpm-api.resi-prod.resi-oversea.com", "resi-oversea.com"],
"outboundTag": "direct"
}
]
}
}

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,25 @@
{
"log": {
"loglevel": "warning"
},
"inbounds": [{
"port": 1080,
"listen": "127.0.0.1",
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": false,
"ip": "127.0.0.1"
}
}],
"outbounds": [{
"protocol": "freedom",
"settings": {},
"tag": "direct"
}],
"policy": {
"levels": {
"0": {"uplinkOnly": 0}
}
}
}

View File

@ -0,0 +1,33 @@
{
"inbounds": [{
"port": 10086,
"protocol": "vmess",
"settings": {
"clients": [
{
"id": "23ad6b10-8d1a-40f7-8ad0-e3e35cd38297",
"level": 1,
"alterId": 64
}
]
}
}],
"outbounds": [{
"protocol": "freedom",
"tag": "direct",
"settings": {}
},{
"protocol": "blackhole",
"settings": {},
"tag": "blocked"
}],
"routing": {
"rules": [
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "blocked"
}
]
}
}

View File

@ -1,12 +1,22 @@
package com.example.studyapp;
import android.app.Activity;
import android.app.AlertDialog;
import android.net.Uri;
import android.net.VpnService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.VpnService;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import android.Manifest;
@ -14,16 +24,14 @@ import android.content.pm.PackageManager;
import android.os.Environment;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
import com.example.studyapp.proxy.CustomVpnService;
import com.example.studyapp.request.ScriptResultRequest;
import com.example.studyapp.service.CloudPhoneManageService;
import com.example.studyapp.socks.SingBoxLauncher;
import com.example.studyapp.utils.IpUtil;
import com.example.studyapp.utils.ShellUtils;
import java.io.File;
import java.lang.ref.WeakReference;
@ -36,6 +44,8 @@ import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
private static final int VPN_REQUEST_CODE = 100; // Adding the missing constant
private static final int REQUEST_CODE_VPN = 2;
private BroadcastReceiver scriptResultReceiver;
@ -65,36 +75,102 @@ public class MainActivity extends AppCompatActivity {
}
private void startProxyVpn(Context context) {
WeakReference<Context> contextRef = new WeakReference<>(context);
new Thread(() -> {
try {
SingBoxLauncher.getInstance().start(context, IpUtil.safeClientIp());
} catch (Exception e) {
Context ctx = contextRef.get();
if (ctx != null) {
runOnUiThread(() ->
Toast.makeText(ctx, "Failed to start VPN: " + e.getMessage(), Toast.LENGTH_SHORT).show()
);
}
}
}).start();
if (!isNetworkAvailable(context)) {
Toast.makeText(context, "Network is not available", Toast.LENGTH_SHORT).show();
return;
}
if (!(context instanceof Activity)) {
Toast.makeText(context, "Context must be an Activity", Toast.LENGTH_SHORT).show();
return;
}
Activity activity = (Activity) context;
try {
startProxyServer(activity); // 在主线程中调用
} catch (IllegalStateException e) {
Toast.makeText(context, "Failed to start VPN: VPN Service illegal state", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(context, "Failed to start VPN: " + (e.getMessage() != null ? e.getMessage() : "Unknown error"), Toast.LENGTH_SHORT).show();
}
}
private void startProxyServer(Activity activity) {
// 请求 VPN 权限
Intent vpnPrepareIntent = VpnService.prepare(activity);
if (vpnPrepareIntent != null) {
// 如果尚未授予权限请求权限等待结果回调
startActivityForResult(vpnPrepareIntent, VPN_REQUEST_CODE);
} else {
// 如果已经授予权限直接调用 onActivityResult 模拟结果处理
onActivityResult(VPN_REQUEST_CODE, RESULT_OK, null);
}
}
private void showToastOnUiThread(Context context, String message) {
new Handler(Looper.getMainLooper()).post(() ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show());
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show();
if (!isNetworkAvailable(this)) {
Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
return;
}
// 启动 VPN 服务
startProxyVpn(this);
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
// 可选择终止操作或退出程序
finish(); // 假设应用需要此权限才能运行
showPermissionExplanationDialog();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) {
// Permission granted, now you can start your VpnService
Intent intent = new Intent(this, CustomVpnService.class);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.startForegroundService(intent);
} else {
this.startService(intent);
}
} catch (IllegalStateException e) {
e.printStackTrace();
showToastOnUiThread(this, "Failed to start VPN service");
}
} else {
// Permission denied or an error occurred
Log.e("VPNSetup", "VPN permission denied or cancelled by user.");
// Handle denial: show a message to the user, disable VPN functionality, etc.
}
}
private void showPermissionExplanationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Permission Required")
.setMessage("Storage Permission is required for the app to function. Please enable it in Settings.")
.setPositiveButton("Go to Settings", (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
})
.setNegativeButton("Cancel", (dialog, which) -> finish())
.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
@ -103,6 +179,17 @@ public class MainActivity extends AppCompatActivity {
}
}
private boolean isNetworkAvailable(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
Network network = connectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
return false;
}
private void runAutojsScript() {
// 定义脚本文件路径
File scriptFile = new File(Environment.getExternalStorageDirectory(), "脚本/chromium.js");

View File

@ -0,0 +1,273 @@
package com.example.studyapp.proxy;
import android.content.Intent;
import android.net.VpnService;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.example.studyapp.utils.V2rayUtil;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
public class CustomVpnService extends VpnService {
private static final String TUN_ADDRESS = "172.19.0.1"; // TUN IP 地址
private static final int PREFIX_LENGTH = 28; // 子网掩码
private static final int MAX_RETRY = 5;
private ParcelFileDescriptor vpnInterface; // TUN 接口描述符
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
// 检查 V2ray 是否已启动避免重复进程
if (!isV2rayRunning()) {
V2rayUtil.startV2Ray(getApplicationContext());
} else {
Log.w("CustomVpnService", "V2Ray already running, skipping redundant start.");
}
// 启动 VPN 流量服务
startVpn();
} catch (Exception e) {
Log.e("CustomVpnService", "Error in onStartCommand: " + e.getMessage(), e);
stopSelf(); // 发生异常时停止服务
}
return START_STICKY;
}
private boolean isV2rayRunning() {
try {
// 执行系统命令获取当前所有正在运行的进程
Process process = Runtime.getRuntime().exec("ps");
// 读取进程的输出
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
// 检查是否有包含 "v2ray" 的进程
if (line.contains("v2ray")) {
Log.i("CustomVpnService", "V2Ray process found: " + line);
return true;
}
}
}
// 检查完成没有找到 "v2ray" 相关的进程
Log.i("CustomVpnService", "No V2Ray process is running.");
return false;
} catch (IOException e) {
// 捕获异常并记录日志
Log.e("CustomVpnService", "Error checking V2Ray process: " + e.getMessage(), e);
return false;
}
}
private void startVpn() {
try {
// 配置虚拟网卡
Builder builder = new Builder();
// 不再需要手动验证 TUN_ADDRESS PREFIX_LENGTH
// 直接使用系统权限建立虚拟网卡用于 TUN 接口和流量捕获
builder.addAddress(TUN_ADDRESS, PREFIX_LENGTH); // 保证 TUN 接口 IP 地址仍与 v2ray 配置文件保持一致
builder.addRoute("0.0.0.0", 0); // 捕获所有 IPv4 流量
// DNS 部分如果有需要也可以简化或直接保留 v2ray 配置提供的
List<String> dnsServers = getSystemDnsServers();
if (dnsServers.isEmpty()) {
// 如果未能从系统中获取到 DNS 地址添加备用默认值
builder.addDnsServer("8.8.8.8"); // Google DNS
builder.addDnsServer("8.8.4.4");
} else {
for (String dnsServer : dnsServers) {
if (isValidIpAddress(dnsServer)) {
builder.addDnsServer(dnsServer);
}
}
}
// 直接建立 TUN 虚拟接口
vpnInterface = builder.establish();
if (vpnInterface == null) {
Log.e(
"CustomVpnService",
"builder.establish() returned null. Check VpnService.Builder configuration and system state."
);
throw new IllegalStateException("VPN Interface establishment failed");
}
// 核心启动流量转发服务此后转发逻辑由 v2ray 接管
new Thread(() -> handleVpnTraffic(vpnInterface)).start();
} catch (Exception e) {
// 增强日志描述信息方便调试
Log.e(
"CustomVpnService",
"startVpn failed: " + e.getMessage(),
e
);
}
}
// 工具方法判断 IP 地址是否合法
private boolean isValidIpAddress(String ip) {
try {
InetAddress.getByName(ip);
return true;
} catch (UnknownHostException e) {
return false;
}
}
private List<String> getSystemDnsServers() {
List<String> dnsServers = new ArrayList<>();
try {
String command = "getprop | grep dns";
Process process = Runtime.getRuntime().exec(command);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("dns") && line.contains(":")) {
String dns = line.split(":")[1].trim();
if (isValidIpAddress(dns)) { // 添加有效性检查
dnsServers.add(dns);
}
}
}
}
} catch (IOException e) {
// 捕获问题日志
Log.e("CustomVpnService", "Error while fetching DNS servers: " + e.getMessage(), e);
}
// 添加默认 DNS 在无效情况下
if (dnsServers.isEmpty()) {
dnsServers.add("8.8.8.8");
dnsServers.add("8.8.4.4");
dnsServers.add("1.1.1.1");
}
return dnsServers;
}
@Override
public void onDestroy() {
super.onDestroy();
try {
if (vpnInterface != null) {
vpnInterface.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleVpnTraffic(ParcelFileDescriptor vpnInterface) {
if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
throw new IllegalArgumentException("ParcelFileDescriptor is invalid!");
}
byte[] packetData = new byte[32767];
int retryCount = 0;
try (FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor())) {
while (!Thread.currentThread().isInterrupted()) {
try {
int length = inStream.read(packetData);
if (length == -1) {
// EOF
break;
}
if (length > 0) {
boolean handled = processPacket(packetData, length);
if (!handled) {
outStream.write(packetData, 0, length);
}
}
retryCount = 0; // Reset retry count after successful read
} catch (IOException e) {
retryCount++;
Log.e("CustomVpnService", "Error reading packet. Retry attempt " + retryCount, e);
if (retryCount >= MAX_RETRY) { // Add constant definition
Log.e("CustomVpnService", "Max retry reached. Exiting loop.");
break;
}
}
}
} catch (IOException e) {
Log.e("CustomVpnService", "IO error in handleVpnTraffic", e);
} finally {
try {
vpnInterface.close();
} catch (IOException e) {
Log.e("CustomVpnService", "Failed to close vpnInterface", e);
}
}
}
private boolean processPacket(byte[] packetData, int length) {
if (packetData == null || length <= 0 || length > packetData.length) {
Log.w("CustomVpnService", "Invalid packetData or length");
return false;
}
try {
boolean isTcpPacket = checkIfTcpPacket(packetData);
boolean isDnsPacket = checkIfDnsRequest(packetData);
Log.d("CustomVpnService", "Packet Info: TCP=" + isTcpPacket + ", DNS=" + isDnsPacket + ", Length=" + length);
if (isTcpPacket || isDnsPacket) {
Log.i("CustomVpnService", "Forwarding to V2Ray. Packet Length: " + length);
return true;
}
} catch (ArrayIndexOutOfBoundsException e) {
Log.e("CustomVpnService", "Malformed packet data: out of bounds", e);
} catch (IllegalArgumentException e) {
Log.e("CustomVpnService", "Invalid packet content", e);
} catch (Exception e) {
Log.e("CustomVpnService", "Unexpected error during packet processing. Packet Length: " + length, e);
}
return false;
}
private boolean checkIfTcpPacket(byte[] packetData) {
// IPv4 数据包最前面 1 字节的前 4 位是版本号
int version = (packetData[0] >> 4) & 0xF;
if (version != 4) {
return false; // IPv4不处理
}
// IPv4 Protocol 字段位于位置 9值为 6 表示 TCP 协议
int protocol = packetData[9] & 0xFF; // 取无符号位
return protocol == 6; // 6 表示 TCP
}
private boolean checkIfDnsRequest(byte[] packetData) {
// IPv4 UDP 协议号为 17
int protocol = packetData[9] & 0xFF;
if (protocol != 17) {
return false; // 不是 UDP
}
// UDP 源端口在 IPv4 头部后的第 0-1 字节总长度 IPv4 Header 20 字节 + UDP Offset
// IPv4 头长度在第一个字节后四位
int ipHeaderLength = (packetData[0] & 0x0F) * 4;
int sourcePort = ((packetData[ipHeaderLength] & 0xFF) << 8) | (packetData[ipHeaderLength + 1] & 0xFF);
int destPort = ((packetData[ipHeaderLength + 2] & 0xFF) << 8) | (packetData[ipHeaderLength + 3] & 0xFF);
// 检查是否是 UDP DNS 端口 (53)
return sourcePort == 53 || destPort == 53;
}
}

View File

@ -1,4 +1,4 @@
package com.example.studyapp.socks;
package com.example.studyapp.proxy;
import android.content.Context;
import android.content.Intent;
@ -9,6 +9,10 @@ import android.os.HandlerThread;
import android.util.Log;
import com.example.studyapp.utils.ShellUtils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
@ -17,7 +21,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class SingBoxLauncher {
private static SingBoxLauncher instance;
private static volatile SingBoxLauncher instance;
private static final String PKG = "io.nekohasekai.sfa";
private final CountDownLatch latch;
@ -43,10 +47,11 @@ public class SingBoxLauncher {
}
public void shutdown() {
handler.removeCallbacksAndMessages(null);
handlerThread.quitSafely();
}
private void checkVPNApp(int count) {
private void checkVPNApp(int count,Context context) {
if (count > 3) {
Log.e("checkVPNApp", "Invalid count parameter: " + count);
return;
@ -55,7 +60,8 @@ public class SingBoxLauncher {
while (count <= 3) {
try {
if (!isProcessRunning(PKG)) {
startApp(PKG);
startApp(PKG); // 启动应用
handler.postDelayed(() -> checkVPN(context, 0,3), 5000); // 延迟 5
} else {
break;
}
@ -90,7 +96,12 @@ public class SingBoxLauncher {
}
public boolean start(Context context,String originIp) {
checkVPNApp(0);
if (!checkNetwork(context)) {
Log.e("SingBoxLauncher", "Network is not connected. Aborting start.");
return false;
}
checkVPNApp(0,context);
Future<?> future = executorService.submit(() -> {
@ -213,7 +224,7 @@ public class SingBoxLauncher {
intent.putExtra("originIp", originIp);
context.sendBroadcast(intent);
handler.postDelayed(() -> checkVPN(context, 0), 1000);
handler.postDelayed(() -> checkVPN(context, 0,3), 1000);
});
try {
@ -233,6 +244,16 @@ public class SingBoxLauncher {
return isVpnRunning;
}
private boolean checkNetwork(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null && activeNetwork.isConnected();
if (!isConnected) {
Log.e("SingBoxLauncher", "Network unavailable");
return false;
}
return true;
}
public void stop(Context context) {
if (context == null || PKG == null || PKG.trim().isEmpty()) {
Log.e("SingBoxLauncher", "Invalid context or package name.");
@ -259,43 +280,38 @@ public class SingBoxLauncher {
}
}
private void checkVPN(Context context, int count) {
try {
if (checkVPN(context)) {
isVpnRunning = true;
Log.d("VPNCheck", "VPN is running.");
latch.countDown();
} else {
Log.d("VPNCheck", "VPN is not running. Retry count: " + count);
if (count > 3) {
isVpnRunning = false;
latch.countDown();
} else {
final int countFinal = count + 1;
handler.removeCallbacksAndMessages(null); // 清理旧任务
handler.postDelayed(() -> checkVPN(context, countFinal), 1000);
}
}
} catch (Exception e) {
Log.e("VPNCheck", "Error in checkVPN", e);
private void checkVPN(Context context, int count, int maxRetries) {
if (count > maxRetries) {
isVpnRunning = false;
Log.e("VPNCheck", "VPN is not running after " + maxRetries + " retries.");
latch.countDown();
return;
}
if (checkVPN(context)) {
isVpnRunning = true;
Log.d("VPNCheck", "VPN is running.");
latch.countDown();
} else {
Log.d("VPNCheck", "VPN is not running. Retry count: " + count);
handler.postDelayed(() ->
checkVPN(context, count + 1, maxRetries), 2000); // 动态调整重试间隔
}
}
// 检查网络中的 VPN 连接
private boolean checkVPN(Context context) {
ConnectivityManager connectivityManager =
ConnectivityManager connManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager == null) return false;
android.net.Network activeNetwork = connectivityManager.getActiveNetwork();
if (activeNetwork == null) return false;
android.net.NetworkCapabilities networkCapabilities =
connectivityManager.getNetworkCapabilities(activeNetwork);
if (networkCapabilities == null) return false;
return networkCapabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_VPN);
if (connManager != null) {
for (android.net.Network network : connManager.getAllNetworks()) {
android.net.NetworkCapabilities capabilities =
connManager.getNetworkCapabilities(network);
if (capabilities != null && capabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_VPN)) {
return true; // 检测到 VPN 连接
}
}
}
return false;
}
private boolean isVpnRunning;

View File

@ -9,16 +9,16 @@ import java.util.regex.Pattern;
public class IpUtil {
public static boolean isValidIPAddress(String ipAddress) {
if ((ipAddress != null) && (!ipAddress.isEmpty()))
{
return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress);
}
return false;
if (ipAddress == null || ipAddress.isEmpty()) return false;
ipAddress = ipAddress.trim(); // 去除多余字符
return ipAddress.matches(
"^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})(\\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})){3}$"
);
}
public static String getClientIp(String url) {
try {
String s = HttpUtil.requestGet(url);
if (!TextUtils.isEmpty(s) && isValidIPAddress(s))
if (!TextUtils.isEmpty(s))
{
return s;
}
@ -32,12 +32,7 @@ public class IpUtil {
}
public static String safeClientIp() {
String result = getClientIp("http://47.236.153.142/");//SG
if (TextUtils.isEmpty(result))
{
result = getClientIp("http://8.211.204.20/");//UK
}
return result;
return getClientIp("https://get.geojs.io/v1/ip");//SG
}
public static String checkClientIp(String excludeCountry)
@ -60,6 +55,7 @@ public class IpUtil {
}
public static CompletableFuture<String> getClientIpFuture()
{
return CompletableFuture.supplyAsync(IpUtil::safeClientIp);

View File

@ -55,7 +55,7 @@ public class ShellUtils {
}
public static String execRootCmdAndGetResult(String cmd){
if (cmd == null || cmd.trim().isEmpty() || !isCommandSafe(cmd)) {
if (cmd == null || cmd.trim().isEmpty()) {
Log.e("ShellUtils", "Unsafe or empty command. Aborting execution.");
throw new IllegalArgumentException("Unsafe or empty command.");
}
@ -139,34 +139,36 @@ public class ShellUtils {
return cmd.matches("^[a-zA-Z0-9._/:\\- ]+$");
}
public static void execRootCmds(List<String> cmds) {
public static List<String> execRootCmds(List<String> cmds) {
List<String> results = new ArrayList<>();
Process process = null;
try {
if (hasBin("su")) {
process = Runtime.getRuntime().exec("su");
} else if (hasBin("xu")) {
process = Runtime.getRuntime().exec("xu");
} else if (hasBin("vu")) {
process = Runtime.getRuntime().exec("vu");
} else {
process = Runtime.getRuntime().exec("sh");
}
try (OutputStream os = process.getOutputStream()) {
// 初始化 Shell 环境
process = hasBin("su") ? Runtime.getRuntime().exec("su") : Runtime.getRuntime().exec("sh");
try (OutputStream os = process.getOutputStream();
InputStream is = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
for (String cmd : cmds) {
Log.e("ShellUtils", "Executing command");
Log.d("ShellUtils", "Executing command: " + cmd);
os.write((cmd + "\n").getBytes());
}
os.write("exit\n".getBytes());
os.flush();
process.waitFor();
// 获取命令输出结果
String line;
while ((line = reader.readLine()) != null) {
results.add(line);
Log.d("ShellUtils", "Command output: " + line);
}
}
process.waitFor();
} catch (Exception e) {
e.printStackTrace();
Log.e("ShellUtils", "Error executing commands: " + e.getMessage());
} finally {
if (process != null) {
process.destroy();
}
}
return results;
}
}

View File

@ -0,0 +1,105 @@
package com.example.studyapp.utils;
import android.content.Context;
import android.util.Log;
import com.example.studyapp.MainActivity;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.io.InputStreamReader;
import java.util.List;
public class V2rayUtil {
public static void startV2Ray(Context context) {
// 确保文件存在
ensureV2RayFilesExist(context);
try {
// 获取文件路径
File fileDir = context.getFilesDir();
File v2rayBinary = new File(fileDir, "v2ray/v2ray");
File v2rayConfig = new File(fileDir, "v2ray/config.json");
// 检查文件存在性再次验证
if (!v2rayBinary.exists() || !v2rayConfig.exists()) {
Log.e("V2Ray", "V2Ray binary or config file not found");
return;
}
// 检查权限
if (!v2rayBinary.setExecutable(true)) {
throw new IllegalStateException("Failed to make V2Ray binary executable");
}
// 构建命令
ProcessBuilder builder = new ProcessBuilder()
.command(v2rayBinary.getAbsolutePath(),
"-config",
v2rayConfig.getAbsolutePath())
.directory(fileDir);
// 其余逻辑不变...
Process process = builder.start();
// 捕获输出逻辑略...
Log.i("V2Ray", "V2Ray service started");
} catch (Exception e) {
Log.e("V2Ray", "Failed to start V2Ray core", e);
}
}
private static void ensureV2RayFilesExist(Context context) {
File filesDir = context.getFilesDir();
File v2rayDir = new File(filesDir, "v2ray");
File v2rayBinary = new File(v2rayDir, "v2ray");
File v2rayConfig = new File(v2rayDir, "config.json");
try {
// 创建 v2ray 目录
if (!v2rayDir.exists() && v2rayDir.mkdirs()) {
Log.i("V2Ray", "Created directory: " + v2rayDir.getAbsolutePath());
}
// 检查并复制 v2ray 可执行文件
if (!v2rayBinary.exists()) {
try (InputStream input = context.getAssets().open("v2ray/v2ray");
FileOutputStream output = new FileOutputStream(v2rayBinary)) {
byte[] buffer = new byte[1024];
int length;
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
Log.i("V2Ray", "Copied v2ray binary to: " + v2rayBinary.getAbsolutePath());
}
}
// 确保可执行权限
if (!v2rayBinary.setExecutable(true)) {
throw new IllegalStateException("Failed to make v2ray binary executable");
}
// 检查并复制 config.json 配置文件
if (!v2rayConfig.exists()) {
try (InputStream input = context.getAssets().open("v2ray/config.json");
FileOutputStream output = new FileOutputStream(v2rayConfig)) {
byte[] buffer = new byte[1024];
int length;
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
Log.i("V2Ray", "Copied v2ray config to: " + v2rayConfig.getAbsolutePath());
}
}
} catch (IOException e) {
Log.e("V2Ray", "Failed to prepare V2Ray files", e);
}
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">47.236.153.142</domain>
<domain includeSubdomains="true">8.211.204.20</domain>
</domain-config>
</network-security-config>