diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..35eb1dd 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 3a46911..414c880 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,6 +21,11 @@ android {
arguments "-DANDROID_ABI=arm64-v8a"
}
}
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['src/main/jniLibs']
+ }
+ }
}
buildTypes {
@@ -58,6 +63,7 @@ dependencies {
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
+ implementation 'androidx.work:work-runtime:2.9.0'
// Retrofit 核心库
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
@@ -66,4 +72,4 @@ dependencies {
// 如果需要 RxJava 支持(可选)
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
-}
\ No newline at end of file
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e65d736..955202d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,12 +20,6 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/example/studyapp/MainActivity.java b/app/src/main/java/com/example/studyapp/MainActivity.java
index 32e5a2e..ff3625c 100644
--- a/app/src/main/java/com/example/studyapp/MainActivity.java
+++ b/app/src/main/java/com/example/studyapp/MainActivity.java
@@ -3,8 +3,6 @@ package com.example.studyapp;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
-import android.content.ComponentName;
-import android.content.ServiceConnection;
import android.net.Uri;
import android.net.VpnService;
import android.content.Context;
@@ -24,19 +22,22 @@ import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Environment;
-import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
import com.example.studyapp.autoJS.AutoJsUtil;
import com.example.studyapp.device.ChangeDeviceInfo;
import com.example.studyapp.proxy.CustomVpnService;
import com.example.studyapp.utils.ReflectionHelper;
+import com.example.studyapp.worker.CheckAccessibilityWorker;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
@@ -49,6 +50,7 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
+ System.setProperty("java.library.path", this.getApplicationInfo().nativeLibraryDir);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// 针对 Android 10 或更低版本检查普通存储权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
@@ -110,6 +112,9 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "resetDeviceInfo button not found", Toast.LENGTH_SHORT).show();
}
+ PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(CheckAccessibilityWorker.class, 15, TimeUnit.MINUTES)
+ .build();
+ WorkManager.getInstance(this).enqueue(workRequest);
}
private void startProxyVpn(Context context) {
@@ -299,4 +304,4 @@ public class MainActivity extends AppCompatActivity {
}
return false;
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/studyapp/config/ConfigLoader.java b/app/src/main/java/com/example/studyapp/config/ConfigLoader.java
index 24c46f6..40ec0c2 100644
--- a/app/src/main/java/com/example/studyapp/config/ConfigLoader.java
+++ b/app/src/main/java/com/example/studyapp/config/ConfigLoader.java
@@ -15,10 +15,10 @@ import java.io.InputStreamReader;
public class ConfigLoader {
// 从 assets 中读取 JSON 文件并解析
- public static String getTunnelAddress() {
+ public static String getTunnelAddress(Context context) {
String jsonStr;
// 获取应用私有目录的文件路径
- File configFile = new File("/data/v2ray/config.json");
+ File configFile = new File(context.getCodeCacheDir(),"config.json");
// 检查文件是否存在
if (!configFile.exists()) {
diff --git a/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java b/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java
index 80d365d..114f7c1 100644
--- a/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java
+++ b/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java
@@ -122,6 +122,12 @@ public class ChangeDeviceInfo {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
+ if (key == null || key.isEmpty()) {
+ throw new IllegalArgumentException("Key cannot be null or empty");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null");
+ }
try {
// 获取类对象
@@ -133,20 +139,21 @@ public class ChangeDeviceInfo {
putStringMethod.invoke(null, context.getContentResolver(), key, value);
Log.d("Debug", "putString executed successfully.");
} catch (ClassNotFoundException e) {
- Log.e("Reflection Error", "Class not found", e);
+ Log.w("Reflection Error", "Class not found: android.provider.VCloudSettings$Global. This may not be supported on this device.");
} catch (NoSuchMethodException e) {
- Log.e("Reflection Error", "Method not found", e);
+ Log.w("Reflection Error", "Method putString not available. Ensure your target Android version supports it.");
} catch (InvocationTargetException e) {
- Throwable cause = e.getTargetException(); // 获取异常的根原因
+ Throwable cause = e.getTargetException();
if (cause instanceof SecurityException) {
- Log.e("Reflection Error", "SecurityException: Permission denied. You need WRITE_SECURE_SETTINGS.", cause);
+ Log.e("Reflection Error", "Permission denied. Ensure WRITE_SECURE_SETTINGS permission is granted.", cause);
} else {
Log.e("Reflection Error", "InvocationTargetException during putString invocation", e);
}
} catch (Exception e) {
- Log.e("Reflection Error", "Unexpected error during putString invocation", e);
+ Log.e("Reflection Error", "Unexpected error during putString invocation: " + e.getMessage());
}
}
+
public static void resetChangedDeviceInfo(String current_pkg_name,Context context) {
try {
Native.setBootId("00000000000000000000000000000000");
diff --git a/app/src/main/java/com/example/studyapp/device/设备参数.txt b/app/src/main/java/com/example/studyapp/device/bigo设备参数.txt
similarity index 100%
rename from app/src/main/java/com/example/studyapp/device/设备参数.txt
rename to app/src/main/java/com/example/studyapp/device/bigo设备参数.txt
diff --git a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java b/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java
index a31059b..405e9bc 100644
--- a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java
+++ b/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java
@@ -1,8 +1,6 @@
package com.example.studyapp.proxy;
import static com.example.studyapp.utils.IpUtil.isValidIpAddress;
-import static com.example.studyapp.utils.V2rayUtil.isV2rayRunning;
-
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -12,30 +10,21 @@ 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 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 = ConfigLoader.getTunnelAddress(); // TUN 的 IP 地址
private static final int PREFIX_LENGTH = 28; // 子网掩码
private Thread vpnTrafficThread; // 保存线程引用
@@ -53,7 +42,7 @@ public class CustomVpnService extends VpnService {
// 开始前台服务
startForeground(NOTIFICATION_ID, createNotification());
try {
- // 检查 V2ray 是否已启动,避免重复进程
+ // 检查 Singbox 是否已启动,避免重复进程
SingboxUtil.startSingBox(getApplicationContext());
// 启动 VPN 流量服务
@@ -72,10 +61,10 @@ public class CustomVpnService extends VpnService {
// 不再需要手动验证 TUN_ADDRESS 和 PREFIX_LENGTH
// 直接使用系统权限建立虚拟网卡,用于 TUN 接口和流量捕获
- builder.addAddress(TUN_ADDRESS, PREFIX_LENGTH); // 保证 TUN 接口 IP 地址仍与 v2ray 配置文件保持一致
+ builder.addAddress(ConfigLoader.getTunnelAddress(this), PREFIX_LENGTH); // 保证 TUN 接口 IP 地址仍与 singbox 配置文件保持一致
builder.addRoute("0.0.0.0", 0); // 捕获所有 IPv4 流量
- // DNS 部分,如果有需要,也可以简化或直接保留 v2ray 配置提供的
+ // DNS 部分,如果有需要,也可以简化或直接保留 singbox 配置提供的
List dnsServers = getSystemDnsServers();
if (dnsServers.isEmpty()) {
// 如果未能从系统中获取到 DNS 地址,添加备用默认值
@@ -100,7 +89,7 @@ public class CustomVpnService extends VpnService {
throw new IllegalStateException("VPN Interface establishment failed");
}
- // 核心:启动流量转发服务,此后转发逻辑由 v2ray 接管
+ // 核心:启动流量转发服务,此后转发逻辑由 singbox 接管
new Thread(() -> handleVpnTraffic(vpnInterface)).start();
} catch (Exception e) {
@@ -175,7 +164,7 @@ public class CustomVpnService extends VpnService {
Log.d("CustomVpnService", "Packet Info: TCP=" + isTcpPacket + ", DNS=" + isDnsPacket + ", Length=" + length);
if (isTcpPacket || isDnsPacket) {
- Log.i("CustomVpnService", "Forwarding to V2Ray. Packet Length: " + length);
+ Log.i("CustomVpnService", "Forwarding to Singbox. Packet Length: " + length);
return true;
}
} catch (ArrayIndexOutOfBoundsException e) {
@@ -215,7 +204,7 @@ public class CustomVpnService extends VpnService {
vpnInterface = null; // 避免资源泄露
}
- // 停止 V2Ray 服务
+ // 停止 Singbox 服务
SingboxUtil.stopSingBox();
Log.i("CustomVpnService", "VPN 服务已停止");
return true;
@@ -248,7 +237,7 @@ public class CustomVpnService extends VpnService {
vpnInterface = null; // 避免资源泄露
}
- // 停止 V2Ray 服务
+ // 停止 Singbox 服务
SingboxUtil.stopSingBox();
Log.i("CustomVpnService", "VPN 服务已销毁");
}
@@ -320,18 +309,27 @@ public class CustomVpnService extends VpnService {
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(
- "vpn_service",
+ channelId,
"VPN Service",
- NotificationManager.IMPORTANCE_DEFAULT
+ NotificationManager.IMPORTANCE_LOW // 设置为低优先级,避免打扰用户
);
- notificationManager.createNotificationChannel(channel);
+ channel.setDescription("通知用户 VPN 服务正在运行中");
+
+ // 注册通道
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(channel);
+ }
}
- return new NotificationCompat.Builder(this, "vpn_service")
+
+ return new NotificationCompat.Builder(this, "vpn_service_channel")
.setContentTitle("VPN 服务")
- .setContentText("VPN 正在运行...")
- .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentText("VPN 正在运行中...")
+ .setSmallIcon(R.drawable.ic_launcher_foreground) // 需要替换成实际图标资源
+ .setPriority(NotificationCompat.PRIORITY_LOW) // 设置为低优先级
.build();
}
diff --git a/app/src/main/java/com/example/studyapp/service/MyAccessibilityService.java b/app/src/main/java/com/example/studyapp/service/MyAccessibilityService.java
new file mode 100644
index 0000000..40b975c
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/service/MyAccessibilityService.java
@@ -0,0 +1,62 @@
+package com.example.studyapp.service;
+
+import android.accessibilityservice.AccessibilityService;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Build;
+import android.os.IBinder;
+
+import android.view.accessibility.AccessibilityEvent;
+import androidx.core.app.NotificationCompat;
+import com.example.studyapp.MainActivity;
+import com.example.studyapp.R;
+
+public class MyAccessibilityService extends AccessibilityService {
+
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+
+ }
+
+ @Override
+ public void onInterrupt() {
+
+ }
+
+ @Override
+ protected void onServiceConnected() {
+ super.onServiceConnected();
+ startForegroundService();
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ String channelId = "2";
+ String channelName = "Foreground Service";
+ int importance = NotificationManager.IMPORTANCE_LOW;
+ NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+ }
+
+ private void startForegroundService() {
+ createNotificationChannel();
+ Intent notificationIntent = new Intent(this, MainActivity.class);
+ int pendingIntentFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0;
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, pendingIntentFlags);
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "2")
+ .setContentTitle("无障碍服务运行中")
+ .setContentText("此服务正在持续运行")
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setContentIntent(pendingIntent);
+ android.app.Notification notification = notificationBuilder.build();
+ startForeground(1, notification);
+ }
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java b/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java
index 965ca3b..705a32d 100644
--- a/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java
+++ b/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java
@@ -2,57 +2,70 @@ 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) {
- if (isSingBoxRunning(context)) {
- Log.i("SingBox", "singbox is already running. Skipping start.");
- return;
- }
+ 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;
- }
-
- try {
- ProcessBuilder builder = new ProcessBuilder(
- singBoxBinary.getAbsolutePath(), "run", "-c", singBoxConfig.getAbsolutePath()
- ).redirectErrorStream(true);
-
- Process process = builder.start();
- Log.i("SingBox", "SingBox service started.");
-
- new Thread(() -> {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
- String line;
- while ((line = reader.readLine()) != null) {
- Log.d("SingBox", line);
- }
- } catch (IOException e) {
- Log.e("SingBox", "Error reading process output", e);
+ 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);
}
- }).start();
+ }
+ }
- int exitCode = process.waitFor(); // 等待进程完成
- Log.i("SingBox", "SingBox process exited with code: " + exitCode);
-
- } catch (IOException e) {
- Log.e("SingBox", "Failed to start SingBox process", e);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- Log.e("SingBox", "Process was interrupted", e);
+ // 简单的 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;
}
}
@@ -91,7 +104,7 @@ public class SingboxUtil {
}
// 检查并复制配置文件
- singBoxConfig = new File("/data/singbox/config.json");
+ singBoxConfig = new File(context.getCodeCacheDir(), "config.json");
if (!singBoxConfig.exists()) {
try (InputStream configInputStream = context.getAssets().open("singbox/" + abi + "/config.json");
@@ -119,51 +132,35 @@ public class SingboxUtil {
return false;
}
- // 判断是否有正在运行的 singbox
- public static boolean isSingBoxRunning(Context context) {
- android.app.ActivityManager activityManager = (android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
- java.util.List runningProcesses = activityManager.getRunningAppProcesses();
- if (runningProcesses != null) {
- for (android.app.ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
- if (processInfo.processName.contains("sing-box")) {
- return true;
- }
- }
- }
- return false;
- }
-
public static void stopSingBox() {
- Process process = null;
- try {
- // 检查是否支持 `pkill` 并停止 sing-box 进程
- ProcessBuilder builder = new ProcessBuilder("pkill", "-f", "sing-box");
- process = builder.start();
-
- // 等待进程执行完成
- int exitCode = process.waitFor();
- if (exitCode == 0) {
- Log.i("SingBox", "singbox process stopped successfully.");
- } else {
- Log.e("SingBox", "Failed to stop singbox process. Exit code: " + exitCode);
+ synchronized (SingboxUtil.class) { // 防止并发冲突
+ if (!isLibraryLoaded) {
+ Log.e("SingBox", "Native library not loaded. Cannot perform StopVpn.");
+ return;
}
- } catch (IOException e) {
- Log.e("SingBox", "IOException occurred while trying to stop singbox", e);
- } catch (InterruptedException e) {
- Log.e("SingBox", "The stopSingBox process was interrupted", e);
- Thread.currentThread().interrupt(); // 恢复中断状态
- } catch (Exception e) {
- Log.e("SingBox", "Unexpected error occurred", e);
- } finally {
- // 确保关闭流以避免资源泄露
- if (process != null) {
- try {
- process.getInputStream().close();
- process.getErrorStream().close();
- } catch (IOException e) {
- Log.e("SingBox", "Failed to close process streams", e);
+
+ 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);
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/studyapp/worker/CheckAccessibilityWorker.java b/app/src/main/java/com/example/studyapp/worker/CheckAccessibilityWorker.java
new file mode 100644
index 0000000..6face92
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/worker/CheckAccessibilityWorker.java
@@ -0,0 +1,70 @@
+package com.example.studyapp.worker;
+
+import android.accessibilityservice.AccessibilityService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.provider.Settings;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.work.CoroutineWorker;
+import androidx.work.WorkerParameters;
+import com.example.studyapp.service.MyAccessibilityService;
+import kotlin.coroutines.Continuation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class CheckAccessibilityWorker extends CoroutineWorker {
+
+ public CheckAccessibilityWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ public boolean isAccessibilityServiceEnabled(Context context, Class extends AccessibilityService> service) {
+ String enabledServices = Settings.Secure.getString(
+ context.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
+ );
+
+ // 检查是否获取到了内容并进行处理
+ if (TextUtils.isEmpty(enabledServices)) {
+ return false;
+ }
+
+ // 利用 split 高效解析字符串
+ String[] components = enabledServices.split(":");
+ String expectedComponentName = new ComponentName(context.getPackageName(), service.getCanonicalName()).flattenToString();
+
+ // 使用 foreach 检查是否匹配
+ for (String componentName : components) {
+ if (expectedComponentName.equalsIgnoreCase(componentName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ @Override
+ public @Nullable Object doWork(@NotNull Continuation super Result> continuation) {
+ if (!isAccessibilityServiceEnabled(getApplicationContext(), MyAccessibilityService.class)) {
+ // 判断是否已经提示过用户引导开启
+ SharedPreferences sharedPreferences = getApplicationContext()
+ .getSharedPreferences("my_app_prefs", Context.MODE_PRIVATE);
+ boolean hasPrompted = sharedPreferences.getBoolean("accessibility_prompted", false);
+
+ if (!hasPrompted) {
+ // 引导用户打开辅助功能服务
+ Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ getApplicationContext().startActivity(intent);
+
+ // 更新状态
+ sharedPreferences.edit().putBoolean("accessibility_prompted", true).apply();
+ }
+ return Result.retry();
+ }
+ return Result.success();
+ }
+}
diff --git a/app/src/main/jniLibs/arm64-v8a/libnative.so b/app/src/main/jniLibs/arm64-v8a/libnative.so
index 7b35fdd..6bebff1 100644
Binary files a/app/src/main/jniLibs/arm64-v8a/libnative.so and b/app/src/main/jniLibs/arm64-v8a/libnative.so differ
diff --git a/app/src/main/jniLibs/arm64-v8a/libsingbox.h b/app/src/main/jniLibs/arm64-v8a/libsingbox.h
new file mode 100644
index 0000000..9d66108
--- /dev/null
+++ b/app/src/main/jniLibs/arm64-v8a/libsingbox.h
@@ -0,0 +1,101 @@
+/* Code generated by cmd/cgo; DO NOT EDIT. */
+
+/* package command-line-arguments */
+
+
+#line 1 "cgo-builtin-export-prolog"
+
+#include
+
+#ifndef GO_CGO_EXPORT_PROLOGUE_H
+#define GO_CGO_EXPORT_PROLOGUE_H
+
+#ifndef GO_CGO_GOSTRING_TYPEDEF
+typedef struct { const char *p; ptrdiff_t n; } _GoString_;
+#endif
+
+#endif
+
+/* Start of preamble from import "C" comments. */
+
+
+#line 3 "singbox.go"
+
+#include // 声明 strlen
+
+#line 1 "cgo-generated-wrapper"
+
+
+/* End of preamble from import "C" comments. */
+
+
+/* Start of boilerplate cgo prologue. */
+#line 1 "cgo-gcc-export-header-prolog"
+
+#ifndef GO_CGO_PROLOGUE_H
+#define GO_CGO_PROLOGUE_H
+
+typedef signed char GoInt8;
+typedef unsigned char GoUint8;
+typedef short GoInt16;
+typedef unsigned short GoUint16;
+typedef int GoInt32;
+typedef unsigned int GoUint32;
+typedef long long GoInt64;
+typedef unsigned long long GoUint64;
+typedef GoInt64 GoInt;
+typedef GoUint64 GoUint;
+typedef size_t GoUintptr;
+typedef float GoFloat32;
+typedef double GoFloat64;
+#ifdef _MSC_VER
+#include
+typedef _Fcomplex GoComplex64;
+typedef _Dcomplex GoComplex128;
+#else
+typedef float _Complex GoComplex64;
+typedef double _Complex GoComplex128;
+#endif
+
+/*
+ static assertion to make sure the file is being used on architecture
+ at least with matching size of GoInt.
+*/
+typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
+
+#ifndef GO_CGO_GOSTRING_TYPEDEF
+typedef _GoString_ GoString;
+#endif
+typedef void *GoMap;
+typedef void *GoChan;
+typedef struct { void *t; void *v; } GoInterface;
+typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
+
+#endif
+
+/* End of boilerplate cgo prologue. */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+// startVPN 初始化并启动带有给定配置的 VPN 实例。
+// 接受 JSON 配置字符串作为输入。
+// 返回 0(如果成功),返回 -1 表示失败。
+//
+extern int Java_com_example_studyapp_utils_SingboxUtil_StartVpn();
+
+// stopVPN 停止当前的 VPN 实例,确保线程安全,处理错误和故障。
+// 返回 0(成功),返回 -1 表示实例未初始化或已关闭,返回 -2 表示关闭时发生错误。
+//
+extern int Java_com_example_studyapp_utils_SingboxUtil_StopVpn();
+
+// isrunning 返回当前实例是否运行中。
+// 它是线程安全的。
+//
+extern GoUint8 Java_com_example_studyapp_utils_SingboxUtil_IsRunning();
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/app/src/main/jniLibs/arm64-v8a/libsingbox.so b/app/src/main/jniLibs/arm64-v8a/libsingbox.so
new file mode 100644
index 0000000..23edc8b
Binary files /dev/null and b/app/src/main/jniLibs/arm64-v8a/libsingbox.so differ
diff --git a/app/src/main/res/xml/AccessibilityServiceConfig.xml b/app/src/main/res/xml/AccessibilityServiceConfig.xml
new file mode 100644
index 0000000..4fcb63e
--- /dev/null
+++ b/app/src/main/res/xml/AccessibilityServiceConfig.xml
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 565f8c2..ac77d27 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,4 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ dependencies {
+ // 替换以下版本
+ classpath 'com.android.tools.build:gradle:8.10.1'
+ }
+}
plugins {
alias(libs.plugins.android.application) apply false
-}
\ No newline at end of file
+}
diff --git a/err.log b/err.log
index 1592308..b7ab9aa 100644
--- a/err.log
+++ b/err.log
@@ -1,2 +1,209 @@
-2025-05-30 15:22:09.569 18506-18506 V2rayUtil com.example.studyapp E Failed to create directory: /data/v2ray
-2025-05-30 15:22:09.569 18506-18506 V2Ray com.example.studyapp E V2Ray files are missing, cannot start.
\ No newline at end of file
+2025-06-04 19:05:48.535 19792-19792 SingBox com.example.studyapp I Copied singbox config.json to: /data/user/0/com.example.studyapp/code_cache/config.json
+2025-06-04 19:05:48.536 19792-19792 SingBox com.example.studyapp D Config passed to StartVpn: {
+ "log": { "level": "trace" },
+ "dns": {
+ "final": "cloudflare",
+ "independent_cache": true,
+ "servers": [
+ {
+ "tag": "cloudflare",
+ "address": "tls://1.1.1.1"
+ },
+ {
+ "tag": "local",
+ "address": "tls://1.1.1.1",
+ "detour": "direct"
+ },
+ {
+ "tag": "remote",
+ "address": "fakeip"
+ }
+ ],
+ "rules": [
+ {
+ "server": "local",
+ "outbound": "any"
+ },
+ {
+ "server": "remote",
+ "query_type": ["A", "AAAA"]
+ }
+ ],
+ "fakeip": {
+ "enabled": true,
+ "inet4_range": "198.18.0.0/15",
+ "inet6_range": "fc00::/18"
+ }
+ },
+ "inbounds": [
+ {
+ "type": "tun",
+ "tag": "tun-in",
+ "address": ["172.19.0.1/28"],
+ "auto_route": true,
+ "sniff": true,
+ "strict_route": false,
+ "domain_strategy": "ipv4_only"
+ }
+ ],
+ "outbounds": [
+ {
+ "type": "socks",
+ "tag": "socks-out",
+ "version": "5",
+ "network": "tcp",
+ "udp_over_tcp": {
+ "enabled": true
+ },
+ "username": "cut_team_protoc_vast-zone-custom-region-us",
+ "password": "Leoliu811001",
+ "server": "105bd58a50330382.na.ipidea.online",
+ "server_port": 2333
+ },
+ {
+ "type": "dns",
+ "tag": "dns-out"
+ },
+ {
+ "type": "direct",
+ "tag": "direct"
+ },
+ {
+ "type": "block",
+ "tag": "block"
+ }
+ ],
+ "route": {
+ "final": "socks-out",
+ "auto_detect_interface": true,
+ "rules": [
+ {
+ "protocol": "dns",
+ "outbound": "dns-out"
+ },
+ {
+ "protocol": ["stun", "quic"],
+ "outbound": "block"
+ },
+ {
+ "ip_is_private": true,
+ "outbound": "direct"
+ },
+ {
+ "ip_cidr": "8.217.74.194/32",
+ "outbound": "direct"
+ },
+ {
+ "domain": "cpm-api.resi-prod.resi-oversea.com",
+ "domain_suffix": "resi-oversea.com",
+ "outbound": "direct"
+ }
+ ]
+ }
+ }
+2025-06-04 19:05:48.538 19792-19792 SingBox com.example.studyapp D UTF-8 Bytes (before JNI): [123, 10, 32, 32, 34, 108, 111, 103, 34, 58, 32, 123, 32, 34, 108, 101, 118, 101, 108, 34, 58, 32, 34, 116, 114, 97, 99, 101, 34, 32, 125, 44, 10, 32, 32, 34, 100, 110, 115, 34, 58, 32, 123, 10, 32, 32, 32, 32, 34, 102, 105, 110, 97, 108, 34, 58, 32, 34, 99, 108, 111, 117, 100, 102, 108, 97, 114, 101, 34, 44, 10, 32, 32, 32, 32, 34, 105, 110, 100, 101, 112, 101, 110, 100, 101, 110, 116, 95, 99, 97, 99, 104, 101, 34, 58, 32, 116, 114, 117, 101, 44, 10, 32, 32, 32, 32, 34, 115, 101, 114, 118, 101, 114, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 99, 108, 111, 117, 100, 102, 108, 97, 114, 101, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 97, 100, 100, 114, 101, 115, 115, 34, 58, 32, 34, 116, 108, 115, 58, 47, 47, 49, 46, 49, 46, 49, 46, 49, 34, 10, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 108, 111, 99, 97, 108, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 97, 100, 100, 114, 101, 115, 115, 34, 58, 32, 34, 116, 108, 115, 58, 47, 47, 49, 46, 49, 46, 49, 46, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 100, 101, 116, 111, 117, 114, 34, 58, 32, 34, 100, 105, 114, 101, 99, 116, 34, 10, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 114, 101, 109, 111, 116, 101, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 97, 100, 100, 114, 101, 115, 115, 34, 58, 32, 34, 102, 97, 107, 101, 105, 112, 34, 10, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 93, 44, 10, 32, 32, 32, 32, 34, 114, 117, 108, 101, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 101, 114, 118, 101, 114, 34, 58, 32, 34, 108, 111, 99, 97, 108, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 111, 117, 116, 98, 111, 117, 110, 100, 34, 58, 32, 34, 97, 110, 121, 34, 10, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 101, 114, 118, 101, 114, 34, 58, 32, 34, 114, 101, 109, 111, 116, 101, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 113, 117, 101, 114, 121, 95, 116, 121, 112, 101, 34, 58, 32, 91, 34, 65, 34, 44, 32, 34, 65, 65, 65, 65, 34, 93, 10, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 93, 44, 10, 32, 32, 32, 32, 34, 102, 97, 107, 101, 105, 112, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 34, 101, 110, 97, 98, 108, 101, 100, 34, 58, 32, 116, 114, 117, 101, 44, 10, 32, 32, 32, 32, 32, 32, 34, 105, 110, 101, 116, 52, 95, 114, 97, 110, 103, 101, 34, 58, 32, 34, 49, 57, 56, 46, 49, 56, 46, 48, 46, 48, 47, 49, 53, 34, 44, 10, 32, 32, 32, 32, 32, 32, 34, 105, 110, 101, 116, 54, 95, 114, 97, 110, 103, 101, 34, 58, 32, 34, 102, 99, 48, 48, 58, 58, 47, 49, 56, 34, 10, 32, 32, 32, 32, 125, 10, 32, 32, 125, 44, 10, 32, 32, 34, 105, 110, 98, 111, 117, 110, 100, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101, 34, 58, 32, 34, 116, 117, 110, 34, 44, 10, 32, 32, 32, 32, 32, 32, 34, 116, 97, 103, 34, 58, 32, 34, 116, 117, 110, 45, 105, 110, 34, 44, 10, 32, 32, 32, 32, 32, 32, 34, 97, 100, 100, 114, 101, 115, 115, 34, 58, 32, 91, 34, 49, 55, 50, 46, 49, 57, 46, 48, 46, 49, 47, 50, 56, 34, 93, 44, 10, 32, 32, 32, 32, 32, 32, 34, 97, 117, 116, 111, 95, 114, 111, 117, 116, 101, 34, 58, 32, 116, 114, 117, 101, 44, 10, 32, 32, 32, 32, 32, 32, 34, 115, 110, 105, 102, 102, 34, 58, 32, 116, 114, 117, 101, 44, 10, 32, 32, 32, 32, 32, 32, 34, 115, 116, 114, 105, 99, 116, 95, 114, 111, 117, 116, 101, 34, 58, 32, 102, 97, 108, 115, 101, 44, 10, 32, 32, 32, 32, 32, 32, 34, 100, 111, 109, 97, 105, 110, 95, 115, 116, 114, 97, 116, 101, 103, 121, 34, 58, 32, 34, 105, 112, 118, 52, 95, 111, 110, 108, 121, 34, 10, 32, 32, 32, 32, 125, 10, 32, 32, 93, 44, 10, 32, 32, 34, 111, 117, 116, 98, 111, 117, 110, 100, 115, 34, 58, 32, 91, 10, 32, 32, 32, 32, 123, 10, 32, 32, 32, 32, 32, 32, 34, 116, 121, 112, 101
+2025-06-04 19:05:48.538 19792-19792 SingBox com.example.studyapp D Config Java HashCode (for cross-check): -457188023
+2025-06-04 19:05:48.541 19792-19792 SingBox com.example.studyapp E Failed to start SingBox service. Return code: -1, Config: {
+ "log": { "level": "trace" },
+ "dns": {
+ "final": "cloudflare",
+ "independent_cache": true,
+ "servers": [
+ {
+ "tag": "cloudflare",
+ "address": "tls://1.1.1.1"
+ },
+ {
+ "tag": "local",
+ "address": "tls://1.1.1.1",
+ "detour": "direct"
+ },
+ {
+ "tag": "remote",
+ "address": "fakeip"
+ }
+ ],
+ "rules": [
+ {
+ "server": "local",
+ "outbound": "any"
+ },
+ {
+ "server": "remote",
+ "query_type": ["A", "AAAA"]
+ }
+ ],
+ "fakeip": {
+ "enabled": true,
+ "inet4_range": "198.18.0.0/15",
+ "inet6_range": "fc00::/18"
+ }
+ },
+ "inbounds": [
+ {
+ "type": "tun",
+ "tag": "tun-in",
+ "address": ["172.19.0.1/28"],
+ "auto_route": true,
+ "sniff": true,
+ "strict_route": false,
+ "domain_strategy": "ipv4_only"
+ }
+ ],
+ "outbounds": [
+ {
+ "type": "socks",
+ "tag": "socks-out",
+ "version": "5",
+ "network": "tcp",
+ "udp_over_tcp": {
+ "enabled": true
+ },
+ "username": "cut_team_protoc_vast-zone-custom-region-us",
+ "password": "Leoliu811001",
+ "server": "105bd58a50330382.na.ipidea.online",
+ "server_port": 2333
+ },
+ {
+ "type": "dns",
+ "tag": "dns-out"
+ },
+ {
+ "type": "direct",
+ "tag": "direct"
+ },
+ {
+ "type": "block",
+ "tag": "block"
+ }
+ ],
+ "route": {
+ "final": "socks-out",
+ "auto_detect_interface": true,
+ "rules": [
+ {
+ "protocol": "dns",
+ "outbound": "dns-out"
+ },
+ {
+ "protocol": ["stun", "quic"],
+ "outbound": "block"
+ },
+ {
+ "ip_is_private": true,
+ "outbound": "direct"
+ },
+ {
+ "ip_cidr": "8.217.74.194/32",
+ "outbound": "direct"
+ },
+ {
+ "domain": "cpm-api.resi-prod.resi-oversea.com",
+ "domain_suffix": "resi-oversea.com",
+ "outbound": "direct"
+ }
+ ]
+ }
+ }
+2025-06-04 19:05:48.541 19792-20002 GoLog com.example.studyapp E [INFO]: Received Base64 config: ?? ?t
+2025-06-04 19:05:48.542 19792-20002 GoLog com.example.studyapp E [ERROR]: Failed to decode Base64 configuration: illegal base64 data at input byte 0. Possible reasons: Input contains invalid Base64 characters or is improperly formatted.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index acf0863..d238fa8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,4 +19,3 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-