Improve VPN service stability and update V2Ray assets

Enhanced the VPN service by refining error handling, adding retry logic, and improving resource cleanup. Consolidated V2Ray asset management logic, ensured compatibility with device architectures, and adjusted permissions handling for newer Android versions. Renamed and reorganized V2Ray assets for better structure.
```
This commit is contained in:
yjj38 2025-05-27 19:25:48 +08:00
parent b46404bb1e
commit c852142262
14 changed files with 221 additions and 124 deletions

View File

@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-23T10:42:56.974007600Z">
<DropdownSelection timestamp="2025-05-26T09:42:51.679223700Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=89NX0C56X" />
<DeviceId pluginId="Default" identifier="serial=10.255.31.151:5555;connection=cafb3fe2" />
</handle>
</Target>
</DropdownSelection>

View File

@ -5,16 +5,14 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<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_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.VPN_SERVICE" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
@ -45,6 +43,7 @@
</activity>
<service
android:name=".proxy.CustomVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true">
<intent-filter>

View File

@ -45,6 +45,9 @@ 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 ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE = 1001;
private static final int REQUEST_CODE_VPN = 2;
private BroadcastReceiver scriptResultReceiver;
@ -52,23 +55,35 @@ public class MainActivity extends AppCompatActivity {
private ActivityResultLauncher<Intent> vpnRequestLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 检查存储权限
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// 针对 Android 10 或更低版本检查普通存储权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_STORAGE_PERMISSION);
REQUEST_CODE_STORAGE_PERMISSION
);
}
} else {
// 针对 Android 11 及更高版本检查全文件管理权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
// 请求权限
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE);
}
}
// 查找按钮对象
// 初始化按钮
Button runScriptButton = findViewById(R.id.run_script_button);
if (runScriptButton != null) {
runScriptButton.setOnClickListener(view -> runAutojsScript()); // 设置点击事件
runScriptButton.setOnClickListener(v -> runAutojsScript());
} else {
Toast.makeText(this, "Button not found", Toast.LENGTH_SHORT).show();
}
@ -137,6 +152,20 @@ public class MainActivity extends AppCompatActivity {
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
if (!isNetworkAvailable(this)) {
Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
return;
}
// 启动 VPN 服务
startProxyVpn(this);
} else {
// 权限未授予可提示用户
Toast.makeText(this, "请授予所有文件管理权限", Toast.LENGTH_SHORT).show();
}
}
if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) {
// Permission granted, now you can start your VpnService
Intent intent = new Intent(this, CustomVpnService.class);
@ -150,10 +179,6 @@ public class MainActivity extends AppCompatActivity {
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.
}
}

View File

@ -98,6 +98,7 @@ public class CustomVpnService extends VpnService {
}
}
builder.setBlocking(true);
// 直接建立 TUN 虚拟接口
vpnInterface = builder.establish();
if (vpnInterface == null) {
@ -160,17 +161,21 @@ public class CustomVpnService extends VpnService {
return dnsServers;
}
@Override
public void onDestroy() {
super.onDestroy();
try {
if (vpnInterface != null) {
try {
vpnInterface.close();
}
} catch (Exception e) {
e.printStackTrace();
vpnInterface = null;
Log.d("CustomVpnService", "VPN interface closed.");
} catch (IOException e) {
Log.e("CustomVpnService", "Error closing VPN interface", e);
}
}
}
private void handleVpnTraffic(ParcelFileDescriptor vpnInterface) {
if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
@ -178,15 +183,19 @@ public class CustomVpnService extends VpnService {
}
byte[] packetData = new byte[32767];
final int maxRetry = 5; // 定义最大重试次数
int retryCount = 0;
try (FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor())) {
FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor());
// 将流放在 try-with-resources 之外避免循环中被关闭
// 循环处理 VPN 数据包
while (!Thread.currentThread().isInterrupted()) {
try {
int length = inStream.read(packetData);
if (length == -1) {
// EOF
// 读取结束
break;
}
@ -196,26 +205,32 @@ public class CustomVpnService extends VpnService {
outStream.write(packetData, 0, length);
}
}
retryCount = 0; // Reset retry count after successful read
retryCount = 0; // 成功一次后重置重试次数
} catch (IOException e) {
retryCount++;
Log.e("CustomVpnService", "Error reading packet. Retry attempt " + retryCount, e);
if (retryCount >= MAX_RETRY) { // Add constant definition
if (retryCount >= maxRetry) {
Log.e("CustomVpnService", "Max retry reached. Exiting loop.");
break;
}
// 可添加短暂延迟来避免频繁重试
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
Log.e("CustomVpnService", "Thread interrupted during sleep", ie);
}
}
} catch (IOException e) {
Log.e("CustomVpnService", "IO error in handleVpnTraffic", e);
} finally {
}
// 最终关闭 ParcelFileDescriptor
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");
@ -268,6 +283,4 @@ public class CustomVpnService extends VpnService {
// 检查是否是 UDP DNS 端口 (53)
return sourcePort == 53 || destPort == 53;
}
}
}}

View File

@ -1,105 +1,114 @@
package com.example.studyapp.utils;
import android.content.Context;
import android.os.Build;
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");
// 确保文件存在并准备就绪
if (!ensureV2RayFilesExist(context)) {
Log.e("V2Ray", "V2Ray files are missing, cannot start.");
return;
}
// 检查权限
if (!v2rayBinary.setExecutable(true)) {
throw new IllegalStateException("Failed to make V2Ray binary executable");
}
// 获取文件路径
File v2rayBinary = new File(context.getCodeCacheDir(), "v2ray");
File v2rayConfig = new File(context.getCodeCacheDir(), "config.json");
// 构建命令
ProcessBuilder builder = new ProcessBuilder()
.command(v2rayBinary.getAbsolutePath(),
"-config",
v2rayConfig.getAbsolutePath())
.directory(fileDir);
ProcessBuilder builder = new ProcessBuilder(v2rayBinary.getAbsolutePath(), "-config", v2rayConfig.getAbsolutePath()).redirectErrorStream(true);
// 其余逻辑不变...
// 启动进程
try {
Process process = builder.start();
} catch (IOException e) {
Log.e("V2Ray", "Failed to start the process", e);
return;
}
// 捕获输出逻辑略...
// 日志输出
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");
public static boolean ensureV2RayFilesExist(Context context) {
synchronized (V2rayUtil.class) {
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)) {
String abi = Build.SUPPORTED_ABIS[0]; // 获取当前设备支持的 ABI 架构
File binaryOutputFile = new File(context.getCodeCacheDir(), "v2ray");
if (!binaryOutputFile.exists()) {
InputStream binaryInputStream = context.getAssets().open("v2ray/" + abi + "/v2ray");
FileOutputStream binaryOutputStream = new FileOutputStream(binaryOutputFile);
try {
byte[] buffer = new byte[1024];
int length;
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
while ((length = binaryInputStream.read(buffer)) > 0) {
binaryOutputStream.write(buffer, 0, length);
}
Log.i("V2Ray", "Copied v2ray binary to: " + v2rayBinary.getAbsolutePath());
Log.i("V2Ray", "Copied v2ray binary to: " + binaryOutputFile.getAbsolutePath());
} catch (Exception e) {
Log.e("V2rayUtil", "Failed to copy v2ray binary", e);
return false;
} finally {
binaryInputStream.close();
binaryOutputStream.close();
}
}
binaryOutputFile.setExecutable(true, false);
binaryOutputFile.setReadable(true, false);
binaryOutputFile.setWritable(true, false);
// 检查文件是否已经具有可执行权限
if (!binaryOutputFile.canExecute()) {
Log.e("V2rayUtil", "Binary file does not have execute permission. Aborting start.");
return false;
}
// 确保可执行权限
if (!v2rayBinary.setExecutable(true)) {
throw new IllegalStateException("Failed to make v2ray binary executable");
}
// 检查并复制 config.json 文件
// 检查并复制 config.json 配置文件
if (!v2rayConfig.exists()) {
try (InputStream input = context.getAssets().open("v2ray/config.json");
FileOutputStream output = new FileOutputStream(v2rayConfig)) {
File configFile = new File(context.getCodeCacheDir(), "config.json");
if (!configFile.exists()) {
InputStream configInputStream = context.getAssets().open("v2ray/" + abi + "/config.json");
FileOutputStream configOutputStream = new FileOutputStream(configFile);
try {
byte[] buffer = new byte[1024];
int length;
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
while ((length = configInputStream.read(buffer)) > 0) {
configOutputStream.write(buffer, 0, length);
}
Log.i("V2Ray", "Copied v2ray config to: " + v2rayConfig.getAbsolutePath());
Log.i("V2Ray", "Copied v2ray config.json to: " + configFile.getAbsolutePath());
} catch (Exception e) {
Log.e("V2rayUtil", "Failed to copy config.json", e);
return false;
} finally {
configInputStream.close();
configOutputStream.close();
}
}
configFile.setReadable(true, false);
configFile.setWritable(true, false);
return true;
} catch (IOException e) {
Log.e("V2Ray", "Failed to prepare V2Ray files", e);
return false;
}
}
}
}

6
cmd Normal file
View File

@ -0,0 +1,6 @@
V2243A:/ # ls -l /data/user/0/com.example.studyapp/files/
total 37516
-rw-rw-rw- 1 u0_a135 u0_a135 2398 2025-05-27 10:43 config.json
-rw------- 1 u0_a135 u0_a135 24 2025-05-27 10:42 profileInstalled
-rwxrwxrwx 1 u0_a135 u0_a135 38404413 2025-05-27 10:43 v2ray
V2243A:/ #

45
err.log Normal file
View File

@ -0,0 +1,45 @@
2025-05-27 19:04:08.548 301-8923 Vpn system_server I Established by com.example.studyapp on tun0
2025-05-27 19:04:08.551 41957-42156 CustomVpnService com.example.studyapp D Packet Info: TCP=false, DNS=false, Length=76
2025-05-27 19:04:08.553 933-933 GoogleInpu...hodService com...gle.android.inputmethod.latin I GoogleInputMethodService.onStartInput():1247 onStartInput(EditorInfo{EditorInfo{packageName=com.example.studyapp, inputType=0, inputTypeString=NULL, enableLearning=false, autoCorrection=false, autoComplete=false, imeOptions=0, privateImeOptions=null, actionName=UNSPECIFIED, actionLabel=null, initialSelStart=-1, initialSelEnd=-1, initialCapsMode=0, label=null, fieldId=-1, fieldName=null, extras=null, hintText=null, hintLocales=[]}}, false)
2025-05-27 19:04:08.557 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 1 (Ask Gemini)
java.io.IOException: Stream Closed
at java.io.FileInputStream.read(FileInputStream.java:316)
at java.io.FileInputStream.read(FileInputStream.java:292)
at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
2025-05-27 19:04:08.557 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 2 (Ask Gemini)
java.io.IOException: Stream Closed
at java.io.FileInputStream.read(FileInputStream.java:316)
at java.io.FileInputStream.read(FileInputStream.java:292)
at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
2025-05-27 19:04:08.557 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 3 (Ask Gemini)
java.io.IOException: Stream Closed
at java.io.FileInputStream.read(FileInputStream.java:316)
at java.io.FileInputStream.read(FileInputStream.java:292)
at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
2025-05-27 19:04:08.558 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 4 (Ask Gemini)
java.io.IOException: Stream Closed
at java.io.FileInputStream.read(FileInputStream.java:316)
at java.io.FileInputStream.read(FileInputStream.java:292)
at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
2025-05-27 19:04:08.558 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 5 (Ask Gemini)
java.io.IOException: Stream Closed
at java.io.FileInputStream.read(FileInputStream.java:316)
at java.io.FileInputStream.read(FileInputStream.java:292)
at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
2025-05-27 19:04:08.558 41957-42156 CustomVpnService com.example.studyapp E Max retry reached. Exiting loop.
2025-05-27 19:04:08.582 301-1568 WindowManager system_server V getPackagePerformanceMode -- ComponentInfo{com.example.studyapp/com.example.studyapp.MainActivity} -- com.example.studyapp -- mode=0