This commit is contained in:
yjj38 2025-06-09 16:17:53 +08:00
parent c20f4cb515
commit 3bde5cadb7
15 changed files with 496 additions and 987 deletions

View File

@ -0,0 +1,120 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="GoogleStyle" />
</state>
</component>

15
.idea/git_toolbox_prj.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@ -54,8 +54,7 @@
android:name=".service.MyAccessibilityService" android:name=".service.MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false">
android:foregroundServiceType="accessibility" >
<intent-filter> <intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" /> <action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter> </intent-filter>

View File

@ -1,10 +1,8 @@
package com.example.studyapp; package com.example.studyapp;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.net.Uri; import android.net.Uri;
import android.net.VpnService;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
@ -12,10 +10,7 @@ import android.net.Network;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings; import android.provider.Settings;
import android.util.Log;
import android.widget.Button; import android.widget.Button;
import android.widget.Toast; import android.widget.Toast;
import android.Manifest; import android.Manifest;
@ -30,18 +25,15 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.work.PeriodicWorkRequest; import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager; import androidx.work.WorkManager;
import com.example.studyapp.autoJS.AutoJsUtil; import com.example.studyapp.autoJS.AutoJsUtil;
import com.example.studyapp.device.ChangeDeviceInfo; import com.example.studyapp.device.ChangeDeviceInfoUtil;
import com.example.studyapp.proxy.CustomVpnService;
import com.example.studyapp.utils.ReflectionHelper;
import com.example.studyapp.utils.ClashUtil;
import com.example.studyapp.worker.CheckAccessibilityWorker; import com.example.studyapp.worker.CheckAccessibilityWorker;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_STORAGE_PERMISSION = 1; 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 ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE = 1001;
@ -76,6 +68,11 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
finish(); finish();
} }
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(CheckAccessibilityWorker.class, 15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(this).enqueue(workRequest);
// 初始化按钮 // 初始化按钮
Button runScriptButton = findViewById(R.id.run_script_button); Button runScriptButton = findViewById(R.id.run_script_button);
if (runScriptButton != null) { if (runScriptButton != null) {
@ -93,28 +90,43 @@ public class MainActivity extends AppCompatActivity {
Button disconnectButton = findViewById(R.id.disconnectVpnButton); Button disconnectButton = findViewById(R.id.disconnectVpnButton);
if (disconnectButton != null) { if (disconnectButton != null) {
disconnectButton.setOnClickListener(v -> stopProxy(this)); disconnectButton.setOnClickListener(v -> ClashUtil.stopProxy(this));
} else {
Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show();
}
Button switchVpnButton = findViewById(R.id.switchVpnButton);
if (switchVpnButton != null) {
switchVpnButton.setOnClickListener(v -> ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170"));
} else { } else {
Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show();
} }
Button modifyDeviceInfoButton = findViewById(R.id.modifyDeviceInfoButton); Button modifyDeviceInfoButton = findViewById(R.id.modifyDeviceInfoButton);
if (modifyDeviceInfoButton != null) { if (modifyDeviceInfoButton != null) {
modifyDeviceInfoButton.setOnClickListener(v -> ChangeDeviceInfo.changeDeviceInfo(getPackageName(),this)); modifyDeviceInfoButton.setOnClickListener(v -> ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170"));
} else { } else {
Toast.makeText(this, "modifyDeviceInfo button not found", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "modifyDeviceInfo button not found", Toast.LENGTH_SHORT).show();
} }
Button resetDeviceInfoButton = findViewById(R.id.resetDeviceInfoButton); Button resetDeviceInfoButton = findViewById(R.id.resetDeviceInfoButton);
if (resetDeviceInfoButton != null) { if (resetDeviceInfoButton != null) {
resetDeviceInfoButton.setOnClickListener(v -> ChangeDeviceInfo.resetChangedDeviceInfo(getPackageName(),this)); resetDeviceInfoButton.setOnClickListener(v -> ChangeDeviceInfoUtil.resetChangedDeviceInfo(getPackageName(), this));
} else { } else {
Toast.makeText(this, "resetDeviceInfo button not found", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "resetDeviceInfo button not found", Toast.LENGTH_SHORT).show();
} }
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(CheckAccessibilityWorker.class, 15, TimeUnit.MINUTES) // try {
.build(); // if (!ClashUtil.checkProxy(this)) {
WorkManager.getInstance(this).enqueue(workRequest); // startProxyVpn(this);
// }else {
// ClashUtil.switchProxyGroup("GLOBAL","us", "127.0.0.1:6170");
// };
// ChangeDeviceInfoUtil.changeDeviceInfo(getPackageName(), this);
// AutoJsUtil.runAutojsScript(this);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
} }
private void startProxyVpn(Context context) { private void startProxyVpn(Context context) {
@ -130,7 +142,7 @@ public class MainActivity extends AppCompatActivity {
Activity activity = (Activity) context; Activity activity = (Activity) context;
try { try {
startProxyServer(activity); // 在主线程中调用 ClashUtil.startProxy(context); // 在主线程中调用
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
Toast.makeText(context, "Failed to start VPN: VPN Service illegal state", Toast.LENGTH_SHORT).show(); Toast.makeText(context, "Failed to start VPN: VPN Service illegal state", Toast.LENGTH_SHORT).show();
} catch (Exception e) { } catch (Exception e) {
@ -138,23 +150,6 @@ public class MainActivity extends AppCompatActivity {
} }
} }
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 @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@ -168,7 +163,6 @@ public class MainActivity extends AppCompatActivity {
} }
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
@ -177,9 +171,6 @@ public class MainActivity extends AppCompatActivity {
case ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE: case ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE:
handleStoragePermissionResult(resultCode); handleStoragePermissionResult(resultCode);
break; break;
case VPN_REQUEST_CODE:
handleVpnPermissionResult(resultCode);
break;
default: default:
break; break;
} }
@ -194,82 +185,6 @@ public class MainActivity extends AppCompatActivity {
} }
} }
private boolean isServiceRunning(Context context, Class<?> serviceClass) {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (manager != null) {
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(service.service.getClassName())) {
return true;
}
}
}
return false;
}
private void stopProxy(Context context) {
if (context == null) {
Log.e("stopProxy", "上下文为空,无法停止服务");
return;
}
if (!isServiceRunning(context, CustomVpnService.class)) {
Log.w("stopProxy", "服务未运行,无法停止");
return;
}
new Thread(() -> {
boolean isServiceStopped = true;
try {
// 通过反射获取服务实例
Object instance = ReflectionHelper.getInstance("com.example.studyapp.proxy.CustomVpnService", "instance");
if (instance != null) {
// 获取并调用 stopService 方法
Method stopServiceMethod = instance.getClass().getDeclaredMethod("stopService", Intent.class);
stopServiceMethod.invoke(instance, intent);
Log.d("stopProxy", "服务已成功停止");
} else {
isServiceStopped = false;
Log.w("stopProxy", "实例为空,服务可能未启动");
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
isServiceStopped = false;
Log.e("stopProxy", "无法停止服务: " + e.getMessage(), e);
} catch (Exception e) {
isServiceStopped = false;
Log.e("stopProxy", "停止服务时发生未知错误: " + e.getMessage(), e);
}
// 在主线程中更新用户提示
String message = isServiceStopped ? "VPN 服务已停止" : "停止 VPN 服务失败";
new Handler(Looper.getMainLooper()).post(() ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show());
}).start();
}
private Intent intent;
private void handleVpnPermissionResult(int resultCode) {
if (resultCode == RESULT_OK) {
intent = new Intent(this, CustomVpnService.class);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
} catch (IllegalStateException e) {
Log.e("handleVpnPermissionResult", "Failed to start VPN service", e);
showToastOnUiThread(this, "Failed to start VPN service");
}
} else {
// 其他结果代码处理逻辑
showToastOnUiThread(this, "VPN permission denied or failed.");
Log.e("handleVpnPermissionResult", "VPN permission denied or failed with resultCode: " + resultCode);
}
}
private void showPermissionExplanationDialog() { private void showPermissionExplanationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Permission Required") builder.setTitle("Permission Required")

View File

@ -1,16 +1,9 @@
package com.example.studyapp.device; package com.example.studyapp.device;
import android.app.ActivityManager;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.net.ConnectivityManager;
import android.net.ProxyInfo;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.example.studyapp.R;
import com.example.studyapp.utils.ReflectionHelper;
import com.example.studyapp.utils.ShellUtils; import com.example.studyapp.utils.ShellUtils;
import org.json.JSONObject; import org.json.JSONObject;
@ -18,7 +11,7 @@ import org.json.JSONObject;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
public class ChangeDeviceInfo { public class ChangeDeviceInfoUtil {
public static void changeDeviceInfo(String current_pkg_name,Context context) { public static void changeDeviceInfo(String current_pkg_name,Context context) {
// 指定包名优先级高于全局 // 指定包名优先级高于全局
@ -81,12 +74,12 @@ public class ChangeDeviceInfo {
} catch (Throwable e) { } catch (Throwable e) {
Log.e("ChangeDeviceInfo", "Error occurred while changing device info", e); Log.e("ChangeDeviceInfoUtil", "Error occurred while changing device info", e);
throw new RuntimeException("Error occurred in changeDeviceInfo", e); throw new RuntimeException("Error occurred in changeDeviceInfo", e);
} }
if (!ShellUtils.hasRootAccess()) { if (!ShellUtils.hasRootAccess()) {
Log.e("ChangeDeviceInfo", "Root access is required to execute system property changes"); Log.e("ChangeDeviceInfoUtil", "Root access is required to execute system property changes");
return; return;
} }

View File

@ -1,339 +0,0 @@
package com.example.studyapp.proxy;
import static com.example.studyapp.utils.IpUtil.isValidIpAddress;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.net.VpnService;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.example.studyapp.R;
import com.example.studyapp.config.ConfigLoader;
import com.example.studyapp.utils.SingboxUtil;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class CustomVpnService extends VpnService {
private static final int PREFIX_LENGTH = 28; // 子网掩码
private Thread vpnTrafficThread; // 保存线程引用
private ParcelFileDescriptor vpnInterface; // TUN 接口描述符
private static CustomVpnService instance;
private static final int NOTIFICATION_ID = 1;
private volatile boolean isVpnActive = false; // 标志位控制数据包处理逻辑
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
isVpnActive = true; // 服务启动时激活
// 开始前台服务
startForeground(NOTIFICATION_ID, createNotification());
try {
// 检查 Singbox 是否已启动避免重复进程
SingboxUtil.startSingBox(getApplicationContext());
// 启动 VPN 流量服务
startVpn();
} catch (Exception e) {
Log.e("CustomVpnService", "Error in onStartCommand: " + e.getMessage(), e);
stopSelf(); // 发生异常时停止服务
}
return START_STICKY;
}
private void startVpn() {
try {
// 配置虚拟网卡
Builder builder = new Builder();
// 不再需要手动验证 TUN_ADDRESS PREFIX_LENGTH
// 直接使用系统权限建立虚拟网卡用于 TUN 接口和流量捕获
builder.addAddress(ConfigLoader.getTunnelAddress(this), PREFIX_LENGTH); // 保证 TUN 接口 IP 地址仍与 singbox 配置文件保持一致
builder.addRoute("0.0.0.0", 0); // 捕获所有 IPv4 流量
// DNS 部分如果有需要也可以简化或直接保留 singbox 配置提供的
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);
}
}
}
builder.setBlocking(true);
// 直接建立 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");
}
// 核心启动流量转发服务此后转发逻辑由 singbox 接管
new Thread(() -> handleVpnTraffic(vpnInterface)).start();
} catch (Exception e) {
// 增强日志描述信息方便调试
Log.e(
"CustomVpnService",
"startVpn failed: " + e.getMessage(),
e
);
}
}
// 工具方法判断 IP 地址是否合法
private void handleVpnTraffic(ParcelFileDescriptor vpnInterface) {
// 启动处理流量的线程
vpnTrafficThread = new Thread(() -> {
if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
Log.e("CustomVpnService", "ParcelFileDescriptor is invalid!");
return;
}
byte[] packetData = new byte[32767]; // 数据包缓冲区
try (FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor())) {
while (!Thread.currentThread().isInterrupted() && isVpnActive) { // 检查线程是否已中断
int length;
try {
length = inStream.read(packetData);
if (length == -1) {
break; // 读取完成退出
}
} catch (IOException e) {
Log.e("CustomVpnService", "Error reading packet", e);
break; // 读取出错退出循环
}
if (length > 0) {
boolean handled = processPacket(packetData, length);
if (!handled) {
outStream.write(packetData, 0, length); // 未处理的包写回
}
}
}
} catch (IOException e) {
Log.e("CustomVpnService", "Error handling VPN traffic", e);
} finally {
try {
vpnInterface.close();
} catch (IOException e) {
Log.e("CustomVpnService", "Failed to close vpnInterface", e);
}
}
});
vpnTrafficThread.start();
}
private boolean processPacket(byte[] packetData, int length) {
if (!isVpnActive) {
Log.w("CustomVpnService", "VPN is not active. Skipping packet processing.");
return false;
}
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 Singbox. 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;
}
@Override
public boolean stopService(Intent name) {
isVpnActive = false; // 服务停止时停用
super.stopService(name);
// 停止处理数据包的线程
if (vpnTrafficThread != null && vpnTrafficThread.isAlive()) {
vpnTrafficThread.interrupt(); // 中断线程
try {
vpnTrafficThread.join(); // 等待线程停止
} catch (InterruptedException e) {
Log.e("CustomVpnService", "Error while stopping vpnTrafficThread", e);
Thread.currentThread().interrupt(); // 重新设置当前线程的中断状态
}
vpnTrafficThread = null; // 清空线程引用
}
// 关闭 VPN 接口
if (vpnInterface != null) {
try {
vpnInterface.close();
} catch (IOException e) {
Log.e("CustomVpnService", "Error closing VPN interface: " + e.getMessage(), e);
}
vpnInterface = null; // 避免资源泄露
}
// 停止 Singbox 服务
SingboxUtil.stopSingBox();
Log.i("CustomVpnService", "VPN 服务已停止");
return true;
}
@Override
public void onDestroy() {
isVpnActive = false; // 服务销毁时停用
super.onDestroy();
// 停止处理数据包的线程
if (vpnTrafficThread != null && vpnTrafficThread.isAlive()) {
vpnTrafficThread.interrupt(); // 中断线程
try {
vpnTrafficThread.join(); // 等待线程停止
} catch (InterruptedException e) {
Log.e("CustomVpnService", "Error while stopping vpnTrafficThread", e);
Thread.currentThread().interrupt(); // 重新设置当前线程的中断状态
}
vpnTrafficThread = null; // 清空线程引用
}
// 关闭 VPN 接口
if (vpnInterface != null) {
try {
vpnInterface.close();
} catch (IOException e) {
Log.e("CustomVpnService", "Error closing VPN interface: " + e.getMessage(), e);
}
vpnInterface = null; // 避免资源泄露
}
// 停止 Singbox 服务
SingboxUtil.stopSingBox();
Log.i("CustomVpnService", "VPN 服务已销毁");
}
@Override
public void onCreate() {
super.onCreate();
instance = this; // 在创建时将实例存储到静态字段
}
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;
}
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;
}
private Notification createNotification() {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String channelId = "vpn_service_channel";
NotificationChannel channel = new NotificationChannel(
channelId,
"VPN Service",
NotificationManager.IMPORTANCE_LOW // 设置为低优先级避免打扰用户
);
channel.setDescription("通知用户 VPN 服务正在运行中");
// 注册通道
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
return new NotificationCompat.Builder(this, "vpn_service_channel")
.setContentTitle("VPN 服务")
.setContentText("VPN 正在运行中...")
.setSmallIcon(R.drawable.ic_launcher_foreground) // 需要替换成实际图标资源
.setPriority(NotificationCompat.PRIORITY_LOW) // 设置为低优先级
.build();
}
public static CustomVpnService getInstance() {
return instance;
}
}

View File

@ -0,0 +1,133 @@
package com.example.studyapp.utils;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONException;
import org.json.JSONObject;
/**
* @Time: 2025/6/9 11:13
* @Creator: 初屿贤
* @File: ClashUtil
* @Project: study.App
* @Description:
*/
public class ClashUtil {
public static void startProxy(Context context) {
Intent intent = new Intent("com.github.kr328.clash.intent.action.SESSION_CREATE");
intent.putExtra("profile", "default"); // 可选择您在 Clash 中配置的 Profile
context.sendBroadcast(intent);
}
public static void stopProxy(Context context) {
new Thread(() -> {
Intent intent = new Intent("com.github.kr328.clash.intent.action.SESSION_DESTROY");
context.sendBroadcast(intent);
}).start();
}
private static volatile boolean isRunning = false;
private static final BroadcastReceiver clashStatusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
isRunning = intent.getBooleanExtra("isRunning", false);
Log.d("ClashUtil", "Clash Status: " + isRunning);
}
};
public static boolean checkProxy(Context context) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
checkClashStatus(context, latch);
latch.await(); // 等待广播接收器更新状态
return isRunning;
}
public static void checkClashStatus(Context context, CountDownLatch latch) {
IntentFilter intentFilter = new IntentFilter("com.github.kr328.clash.intent.action.SESSION_STATE");
BroadcastReceiver clashStatusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
isRunning = intent.getBooleanExtra("isRunning", false);
Log.d("ClashUtil", "Clash Status: " + isRunning);
latch.countDown(); // 状态更新完成释放锁
}
};
ContextCompat.registerReceiver(context, clashStatusReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
Intent queryIntent = new Intent("com.github.kr328.clash.intent.action.SESSION_QUERY");
context.sendBroadcast(queryIntent);
}
public static void unregisterReceiver(Context context) {
context.unregisterReceiver(clashStatusReceiver);
}
public static void switchProxyGroup(String groupName, String proxyName, String controllerUrl) {
if (groupName == null || groupName.trim().isEmpty() || proxyName == null || proxyName.trim().isEmpty()) {
throw new IllegalArgumentException("Group name and proxy name must not be empty");
}
if (controllerUrl == null || !controllerUrl.matches("^https?://.*")) {
throw new IllegalArgumentException("Invalid controller URL");
}
OkHttpClient client = new OkHttpClient();
JSONObject json = new JSONObject();
try {
json.put("name", proxyName);
} catch (JSONException e) {
e.printStackTrace();
}
String jsonBody = json.toString();
MediaType JSON = MediaType.get("application/json; charset=utf-8");
RequestBody requestBody = RequestBody.create(JSON, jsonBody);
HttpUrl url = HttpUrl.parse(controllerUrl)
.newBuilder()
.addPathSegments("proxies/" + groupName)
.build();
Request request = new Request.Builder()
.url(url)
.put(requestBody)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
System.out.println("Failed to switch proxy: " + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
if (response.body() != null) {
System.out.println("Switch proxy response: " + response.body().string());
} else {
System.out.println("Response body is null");
}
} finally {
response.close();
}
}
});
}
}

View File

@ -1,166 +0,0 @@
package com.example.studyapp.utils;
import android.content.Context;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Base64;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
public class SingboxUtil {
private static boolean isLibraryLoaded = false;
static {
try {
System.loadLibrary("singbox");
isLibraryLoaded = true;
} catch (UnsatisfiedLinkError e) {
Log.e("SingBox", "Failed to load singbox library.", e);
}
}
public static native int StartVpn();
public static native int StopVpn();
private static File singBoxBinary, singBoxConfig;
public static void startSingBox(Context context) {
synchronized (SingboxUtil.class) { // 确保线程安全
if (!isLibraryLoaded) {
Log.e("SingBox", "Native library not loaded. Cannot perform StopVpn.");
return;
}
if (!ensureSingBoxFilesExist(context)) {
Log.e("SingBox", "Singbox files are missing.");
return;
}
int result = StartVpn();
if (result == 0) {
Log.i("SingBox", "SingBox service started successfully using StartVpn.");
} else {
Log.e("SingBox", "Failed to start SingBox service. Return code: " + result);
}
}
}
// 简单的 JSON 验证函数示例
private static boolean isValidJson(String json) {
try {
new org.json.JSONObject(json); // or new org.json.JSONArray(json);
return true;
} catch (org.json.JSONException ex) {
return false;
}
}
// 检查并复制 singbox 必需文件
public static boolean ensureSingBoxFilesExist(Context context) {
synchronized (SingboxUtil.class) {
try {
// 检查并复制 singbox 可执行文件
String abi = Build.SUPPORTED_ABIS[0]; // 获取当前设备支持的 ABI 架构
singBoxBinary = new File(context.getCodeCacheDir(), "singbox");
// 复制二进制文件
if (!singBoxBinary.exists()) {
try (InputStream binaryInputStream = context.getAssets().open("singbox/" + abi + "/sing-box");
FileOutputStream binaryOutputStream = new FileOutputStream(singBoxBinary)) {
byte[] buffer = new byte[1024];
int length;
while ((length = binaryInputStream.read(buffer)) > 0) {
binaryOutputStream.write(buffer, 0, length);
}
Log.i("SingBox", "Copied singbox binary to: " + singBoxBinary.getAbsolutePath());
} catch (Exception e) {
Log.e("SingboxUtil", "Failed to copy singbox binary", e);
return false;
}
}
// 设置执行权限
singBoxBinary.setExecutable(true);
singBoxBinary.setReadable(true);
singBoxBinary.setWritable(true);
if (!singBoxBinary.canExecute()) {
Log.e("SingboxUtil", "Binary file does not have execute permission. Aborting start.");
return false;
}
// 检查并复制配置文件
singBoxConfig = new File(context.getCodeCacheDir(), "config.json");
if (!singBoxConfig.exists()) {
try (InputStream configInputStream = context.getAssets().open("singbox/" + abi + "/config.json");
FileOutputStream configOutputStream = new FileOutputStream(singBoxConfig)) {
byte[] buffer = new byte[1024];
int length;
while ((length = configInputStream.read(buffer)) > 0) {
configOutputStream.write(buffer, 0, length);
}
Log.i("SingBox", "Copied singbox config.json to: " + singBoxConfig.getAbsolutePath());
} catch (Exception e) {
Log.e("SingboxUtil", "Failed to copy config.json", e);
return false;
}
}
singBoxConfig.setReadable(true);
singBoxConfig.setWritable(true);
return true;
} catch (Exception e) {
Log.e("SingboxUtil", "Error in ensureSingBoxFilesExist method", e);
}
}
return false;
}
public static void stopSingBox() {
synchronized (SingboxUtil.class) { // 防止并发冲突
if (!isLibraryLoaded) {
Log.e("SingBox", "Native library not loaded. Cannot perform StopVpn.");
return;
}
try {
Log.d("SingBox", "Invoking StopVpn method on thread: " + Thread.currentThread().getName());
int result = StopVpn();
switch (result) {
case 0:
Log.i("SingBox", "SingBox service stopped successfully using StopVpn.");
break;
case -1:
Log.e("SingBox", "Failed to stop SingBox: Instance is not initialized or already stopped.");
break;
case -2:
Log.e("SingBox", "Failed to stop SingBox: Unexpected error occurred during the shutdown.");
break;
default:
Log.e("SingBox", "Unexpected StopVpn error code: " + result);
break;
}
} catch (UnsatisfiedLinkError e) {
Log.e("SingBox", "Failed to load native library for stopping VPN.", e);
} catch (Exception e) {
Log.e("SingBox", "Unexpected error occurred while stopping SingBox using StopVpn.", e);
}
}
}
}

View File

@ -1,180 +0,0 @@
package com.example.studyapp.utils;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class V2rayUtil {
private static File v2rayConfig, v2rayBinary;
public static void startV2Ray(Context context) {
try {
// 确保文件存在并准备就绪
if (!ensureV2RayFilesExist(context)) {
Log.e("V2Ray", "V2Ray files are missing, cannot start.");
return;
}
// 构建命令
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);
}
}
public static boolean ensureV2RayFilesExist(Context context) {
synchronized (V2rayUtil.class) {
try {
// 检查并复制 v2ray 可执行文件
String abi = Build.SUPPORTED_ABIS[0]; // 获取当前设备支持的 ABI 架构
v2rayBinary = new File(context.getCodeCacheDir(), "v2ray");
if (!v2rayBinary.exists()) {
InputStream binaryInputStream = context.getAssets().open("v2ray/" + abi + "/v2ray");
FileOutputStream binaryOutputStream = new FileOutputStream(v2rayBinary);
try {
byte[] buffer = new byte[1024];
int length;
while ((length = binaryInputStream.read(buffer)) > 0) {
binaryOutputStream.write(buffer, 0, length);
}
Log.i("V2Ray", "Copied v2ray binary to: " + v2rayBinary.getAbsolutePath());
} catch (Exception e) {
Log.e("V2rayUtil", "Failed to copy v2ray binary", e);
return false;
} finally {
binaryInputStream.close();
binaryOutputStream.close();
}
}
v2rayBinary.setExecutable(true, false);
v2rayBinary.setReadable(true, false);
v2rayBinary.setWritable(true, false);
// 检查文件是否已经具有可执行权限
if (!v2rayBinary.canExecute()) {
Log.e("V2rayUtil", "Binary file does not have execute permission. Aborting start.");
return false;
}
// 检查并复制 config.json 文件
v2rayConfig = new File("/data/v2ray/config.json");
File v2rayDirectory = v2rayConfig.getParentFile();
if (v2rayDirectory != null && !v2rayDirectory.exists()) {
Log.e("V2rayUtil", "Failed to find directory: " + v2rayDirectory.getAbsolutePath());
return false; // 无法创建目录时直接返回
}
if (!v2rayConfig.exists()) {
InputStream configInputStream = context.getAssets().open("v2ray/" + abi + "/config.json");
FileOutputStream configOutputStream = new FileOutputStream(v2rayConfig);
try {
byte[] buffer = new byte[1024];
int length;
while ((length = configInputStream.read(buffer)) > 0) {
configOutputStream.write(buffer, 0, length);
}
Log.i("V2Ray", "Copied v2ray config.json to: " + v2rayConfig.getAbsolutePath());
} catch (Exception e) {
Log.e("V2rayUtil", "Failed to copy config.json", e);
return false;
} finally {
configInputStream.close();
configOutputStream.close();
}
}
v2rayConfig.setReadable(true, false);
v2rayConfig.setWritable(true, false);
return true;
} catch (IOException e) {
Log.e("V2Ray", "Failed to prepare V2Ray files", e);
return false;
}
}
}
public static void stopV2Ray() {
// 如果二进制文件不存在或不可执行直接返回
if (v2rayBinary == null || !v2rayBinary.exists() || !v2rayBinary.canExecute()) {
Log.e("V2Ray", "v2rayBinary is either null, does not exist, or is not executable: " +
(v2rayBinary != null ? v2rayBinary.getAbsolutePath() : "null"));
return;
}
// 创建新线程来处理停止任务
new Thread(() -> {
try {
// 判断是否有运行中的 v2ray 进程
if (isV2rayRunning()) {
String command = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "ps -A" : "ps";
Process psProcess = Runtime.getRuntime().exec(command); // 列出所有进程
try (BufferedReader reader = new BufferedReader(new InputStreamReader(psProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
// 检查是否是 v2ray 进程
if (line.contains("v2ray")) {
String[] parts = line.trim().split("\\s+");
if (parts.length > 1) {
String pid = parts[1]; // 获取 PID
Log.i("V2Ray", "Found V2Ray process, PID: " + pid);
// 发出 kill 指令以终止进程
Process killProcess = new ProcessBuilder("kill", "-9", pid).start();
killProcess.waitFor(); // 等待命令完成
Log.i("V2Ray", "V2Ray stopped successfully.");
return; // 停止任务完成后退出
}
}
}
}
}
Log.i("V2Ray", "No V2Ray process is currently running.");
} catch (IOException | InterruptedException e) {
Log.e("V2Ray", "Error while stopping V2Ray: " + e.getMessage(), e);
}
}).start();
}
public static boolean isV2rayRunning() {
try {
String command = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "ps -A" : "ps";
Process process = Runtime.getRuntime().exec(command);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("v2ray")) { // 更精确匹配进程描述
Log.i("CustomVpnService", "V2Ray process found: " + line);
return true;
}
}
}
Log.i("CustomVpnService", "No V2Ray process is running.");
} catch (IOException e) {
Log.e("CustomVpnService", "Error checking V2Ray process: " + e.getMessage(), e);
}
return false;
}
}

View File

@ -1,48 +1,60 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp" android:padding="16dp"
android:background="@drawable/background" android:background="@drawable/background"
tools:context=".MainActivity"> tools:context=".MainActivity">
<Button <LinearLayout
android:id="@+id/run_script_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<Button
android:id="@+id/run_script_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始运行脚本" android:text="开始运行脚本"
android:contentDescription="运行脚本按钮" /> android:contentDescription="运行脚本按钮,用于启动自动化脚本" />
<Button <Button
android:id="@+id/connectVpnButton" android:id="@+id/connectVpnButton"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="开启vpn" android:text="开启 VPN"
android:contentDescription="开启 VPN 按钮" /> android:contentDescription="开启 VPN 按钮,用于连接到指定 VPN 服务" />
<Button <Button
android:id="@+id/disconnectVpnButton" android:id="@+id/disconnectVpnButton"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="断开vpn" android:text="断开 VPN"
android:contentDescription="断开 VPN 按钮" /> android:contentDescription="断开 VPN 按钮,用于断开当前连接的 VPN" />
<Button
android:id="@+id/switchVpnButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="切换 VPN"
android:contentDescription="切换 VPN 按钮,用于从一个 VPN 切换到另一个 VPN" />
<Button <Button
android:id="@+id/modifyDeviceInfoButton" android:id="@+id/modifyDeviceInfoButton"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="修改设备信息" android:text="修改设备信息"
android:contentDescription="修改设备信息按钮" /> android:contentDescription="修改设备信息按钮,用于编辑和更改设备信息" />
<Button <Button
android:id="@+id/resetDeviceInfoButton" android:id="@+id/resetDeviceInfoButton"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="重置设备信息" android:text="重置设备信息"
android:contentDescription="重置设备信息按钮" /> android:contentDescription="重置设备信息按钮,用于恢复到默认的设备信息" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">study.App</string> <string name="app_name">study_app</string>
<string name="accessibility_service_description">This is an accessibility service.</string>
</resources> </resources>

View File

@ -9,5 +9,6 @@
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">47.236.153.142</domain> <domain includeSubdomains="true">47.236.153.142</domain>
<domain includeSubdomains="true">8.211.204.20</domain> <domain includeSubdomains="true">8.211.204.20</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config> </domain-config>
</network-security-config> </network-security-config>