Integrate WorkManager for periodic accessibility service checks

Added `CheckAccessibilityWorker` with WorkManager to periodically validate and prompt accessibility service activation. Registered new `MyAccessibilityService` in manifest and set configurations. Introduced native `Singbox` methods for VPN operations and added compatibility fixes, including JNI library path initialization. Updated dependency `androidx.work:work-runtime:2.9.0`.
This commit is contained in:
yjj38 2025-06-07 09:52:33 +08:00
parent be91f9ea3b
commit c20f4cb515
18 changed files with 599 additions and 127 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="" vcs="Git" />
</component> </component>
</project> </project>

View File

@ -21,6 +21,11 @@ android {
arguments "-DANDROID_ABI=arm64-v8a" arguments "-DANDROID_ABI=arm64-v8a"
} }
} }
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
} }
buildTypes { buildTypes {
@ -58,6 +63,7 @@ dependencies {
androidTestImplementation libs.ext.junit androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core androidTestImplementation libs.espresso.core
implementation 'androidx.work:work-runtime:2.9.0'
// Retrofit // Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'

View File

@ -20,12 +20,6 @@
<uses-permission android:name="android.permission.CREATE_USERS" /> <uses-permission android:name="android.permission.CREATE_USERS" />
<uses-permission android:name="android.permission.QUERY_USERS" /> <uses-permission android:name="android.permission.QUERY_USERS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -50,13 +44,26 @@
</activity> </activity>
<service <service
android:name=".proxy.CustomVpnService" android:name=".proxy.CustomVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService" /> <action android:name="android.net.VpnService" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".service.MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="accessibility" >
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application> </application>
</manifest> </manifest>

View File

@ -3,8 +3,6 @@ package com.example.studyapp;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.ServiceConnection;
import android.net.Uri; import android.net.Uri;
import android.net.VpnService; import android.net.VpnService;
import android.content.Context; import android.content.Context;
@ -24,19 +22,22 @@ import android.Manifest;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Environment; import android.os.Environment;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.work.PeriodicWorkRequest;
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.ChangeDeviceInfo;
import com.example.studyapp.proxy.CustomVpnService; import com.example.studyapp.proxy.CustomVpnService;
import com.example.studyapp.utils.ReflectionHelper; import com.example.studyapp.utils.ReflectionHelper;
import com.example.studyapp.worker.CheckAccessibilityWorker;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
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;
@ -49,6 +50,7 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
System.setProperty("java.library.path", this.getApplicationInfo().nativeLibraryDir);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// 针对 Android 10 或更低版本检查普通存储权限 // 针对 Android 10 或更低版本检查普通存储权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) 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(); 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) { private void startProxyVpn(Context context) {

View File

@ -15,10 +15,10 @@ import java.io.InputStreamReader;
public class ConfigLoader { public class ConfigLoader {
// assets 中读取 JSON 文件并解析 // assets 中读取 JSON 文件并解析
public static String getTunnelAddress() { public static String getTunnelAddress(Context context) {
String jsonStr; String jsonStr;
// 获取应用私有目录的文件路径 // 获取应用私有目录的文件路径
File configFile = new File("/data/v2ray/config.json"); File configFile = new File(context.getCodeCacheDir(),"config.json");
// 检查文件是否存在 // 检查文件是否存在
if (!configFile.exists()) { if (!configFile.exists()) {

View File

@ -122,6 +122,12 @@ public class ChangeDeviceInfo {
if (context == null) { if (context == null) {
throw new IllegalArgumentException("Context cannot be 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 { try {
// 获取类对象 // 获取类对象
@ -133,20 +139,21 @@ public class ChangeDeviceInfo {
putStringMethod.invoke(null, context.getContentResolver(), key, value); putStringMethod.invoke(null, context.getContentResolver(), key, value);
Log.d("Debug", "putString executed successfully."); Log.d("Debug", "putString executed successfully.");
} catch (ClassNotFoundException e) { } 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) { } 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) { } catch (InvocationTargetException e) {
Throwable cause = e.getTargetException(); // 获取异常的根原因 Throwable cause = e.getTargetException();
if (cause instanceof SecurityException) { 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 { } else {
Log.e("Reflection Error", "InvocationTargetException during putString invocation", e); Log.e("Reflection Error", "InvocationTargetException during putString invocation", e);
} }
} catch (Exception 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) { public static void resetChangedDeviceInfo(String current_pkg_name,Context context) {
try { try {
Native.setBootId("00000000000000000000000000000000"); Native.setBootId("00000000000000000000000000000000");

View File

@ -1,8 +1,6 @@
package com.example.studyapp.proxy; package com.example.studyapp.proxy;
import static com.example.studyapp.utils.IpUtil.isValidIpAddress; import static com.example.studyapp.utils.IpUtil.isValidIpAddress;
import static com.example.studyapp.utils.V2rayUtil.isV2rayRunning;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@ -12,30 +10,21 @@ import android.net.VpnService;
import android.os.Build; import android.os.Build;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.util.Log; import android.util.Log;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import com.example.studyapp.R; import com.example.studyapp.R;
import com.example.studyapp.config.ConfigLoader; import com.example.studyapp.config.ConfigLoader;
import com.example.studyapp.utils.SingboxUtil; import com.example.studyapp.utils.SingboxUtil;
import com.example.studyapp.utils.V2rayUtil;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class CustomVpnService extends VpnService { public class CustomVpnService extends VpnService {
private static final String TUN_ADDRESS = ConfigLoader.getTunnelAddress(); // TUN IP 地址
private static final int PREFIX_LENGTH = 28; // 子网掩码 private static final int PREFIX_LENGTH = 28; // 子网掩码
private Thread vpnTrafficThread; // 保存线程引用 private Thread vpnTrafficThread; // 保存线程引用
@ -53,7 +42,7 @@ public class CustomVpnService extends VpnService {
// 开始前台服务 // 开始前台服务
startForeground(NOTIFICATION_ID, createNotification()); startForeground(NOTIFICATION_ID, createNotification());
try { try {
// 检查 V2ray 是否已启动避免重复进程 // 检查 Singbox 是否已启动避免重复进程
SingboxUtil.startSingBox(getApplicationContext()); SingboxUtil.startSingBox(getApplicationContext());
// 启动 VPN 流量服务 // 启动 VPN 流量服务
@ -72,10 +61,10 @@ public class CustomVpnService extends VpnService {
// 不再需要手动验证 TUN_ADDRESS PREFIX_LENGTH // 不再需要手动验证 TUN_ADDRESS PREFIX_LENGTH
// 直接使用系统权限建立虚拟网卡用于 TUN 接口和流量捕获 // 直接使用系统权限建立虚拟网卡用于 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 流量 builder.addRoute("0.0.0.0", 0); // 捕获所有 IPv4 流量
// DNS 部分如果有需要也可以简化或直接保留 v2ray 配置提供的 // DNS 部分如果有需要也可以简化或直接保留 singbox 配置提供的
List<String> dnsServers = getSystemDnsServers(); List<String> dnsServers = getSystemDnsServers();
if (dnsServers.isEmpty()) { if (dnsServers.isEmpty()) {
// 如果未能从系统中获取到 DNS 地址添加备用默认值 // 如果未能从系统中获取到 DNS 地址添加备用默认值
@ -100,7 +89,7 @@ public class CustomVpnService extends VpnService {
throw new IllegalStateException("VPN Interface establishment failed"); throw new IllegalStateException("VPN Interface establishment failed");
} }
// 核心启动流量转发服务此后转发逻辑由 v2ray 接管 // 核心启动流量转发服务此后转发逻辑由 singbox 接管
new Thread(() -> handleVpnTraffic(vpnInterface)).start(); new Thread(() -> handleVpnTraffic(vpnInterface)).start();
} catch (Exception e) { } catch (Exception e) {
@ -175,7 +164,7 @@ public class CustomVpnService extends VpnService {
Log.d("CustomVpnService", "Packet Info: TCP=" + isTcpPacket + ", DNS=" + isDnsPacket + ", Length=" + length); Log.d("CustomVpnService", "Packet Info: TCP=" + isTcpPacket + ", DNS=" + isDnsPacket + ", Length=" + length);
if (isTcpPacket || isDnsPacket) { if (isTcpPacket || isDnsPacket) {
Log.i("CustomVpnService", "Forwarding to V2Ray. Packet Length: " + length); Log.i("CustomVpnService", "Forwarding to Singbox. Packet Length: " + length);
return true; return true;
} }
} catch (ArrayIndexOutOfBoundsException e) { } catch (ArrayIndexOutOfBoundsException e) {
@ -215,7 +204,7 @@ public class CustomVpnService extends VpnService {
vpnInterface = null; // 避免资源泄露 vpnInterface = null; // 避免资源泄露
} }
// 停止 V2Ray 服务 // 停止 Singbox 服务
SingboxUtil.stopSingBox(); SingboxUtil.stopSingBox();
Log.i("CustomVpnService", "VPN 服务已停止"); Log.i("CustomVpnService", "VPN 服务已停止");
return true; return true;
@ -248,7 +237,7 @@ public class CustomVpnService extends VpnService {
vpnInterface = null; // 避免资源泄露 vpnInterface = null; // 避免资源泄露
} }
// 停止 V2Ray 服务 // 停止 Singbox 服务
SingboxUtil.stopSingBox(); SingboxUtil.stopSingBox();
Log.i("CustomVpnService", "VPN 服务已销毁"); Log.i("CustomVpnService", "VPN 服务已销毁");
} }
@ -320,18 +309,27 @@ public class CustomVpnService extends VpnService {
private Notification createNotification() { private Notification createNotification() {
NotificationManager notificationManager = NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String channelId = "vpn_service_channel";
NotificationChannel channel = new NotificationChannel( NotificationChannel channel = new NotificationChannel(
"vpn_service", channelId,
"VPN Service", "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 服务") .setContentTitle("VPN 服务")
.setContentText("VPN 正在运行...") .setContentText("VPN 正在运行中...")
.setSmallIcon(R.drawable.ic_launcher_foreground) .setSmallIcon(R.drawable.ic_launcher_foreground) // 需要替换成实际图标资源
.setPriority(NotificationCompat.PRIORITY_LOW) // 设置为低优先级
.build(); .build();
} }

View File

@ -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);
}
}

View File

@ -2,57 +2,70 @@ package com.example.studyapp.utils;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; 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 { 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; private static File singBoxBinary, singBoxConfig;
public static void startSingBox(Context context) { public static void startSingBox(Context context) {
if (isSingBoxRunning(context)) { synchronized (SingboxUtil.class) { // 确保线程安全
Log.i("SingBox", "singbox is already running. Skipping start."); if (!isLibraryLoaded) {
return; Log.e("SingBox", "Native library not loaded. Cannot perform StopVpn.");
} return;
}
if (!ensureSingBoxFilesExist(context)) { if (!ensureSingBoxFilesExist(context)) {
Log.e("SingBox", "Singbox files are missing."); Log.e("SingBox", "Singbox files are missing.");
return; return;
} }
int result = StartVpn();
try { if (result == 0) {
ProcessBuilder builder = new ProcessBuilder( Log.i("SingBox", "SingBox service started successfully using StartVpn.");
singBoxBinary.getAbsolutePath(), "run", "-c", singBoxConfig.getAbsolutePath() } else {
).redirectErrorStream(true); Log.e("SingBox", "Failed to start SingBox service. Return code: " + result);
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);
} }
}).start(); }
}
int exitCode = process.waitFor(); // 等待进程完成 // 简单的 JSON 验证函数示例
Log.i("SingBox", "SingBox process exited with code: " + exitCode); private static boolean isValidJson(String json) {
try {
} catch (IOException e) { new org.json.JSONObject(json); // or new org.json.JSONArray(json);
Log.e("SingBox", "Failed to start SingBox process", e); return true;
} catch (InterruptedException e) { } catch (org.json.JSONException ex) {
Thread.currentThread().interrupt(); return false;
Log.e("SingBox", "Process was interrupted", e);
} }
} }
@ -91,7 +104,7 @@ public class SingboxUtil {
} }
// 检查并复制配置文件 // 检查并复制配置文件
singBoxConfig = new File("/data/singbox/config.json"); singBoxConfig = new File(context.getCodeCacheDir(), "config.json");
if (!singBoxConfig.exists()) { if (!singBoxConfig.exists()) {
try (InputStream configInputStream = context.getAssets().open("singbox/" + abi + "/config.json"); try (InputStream configInputStream = context.getAssets().open("singbox/" + abi + "/config.json");
@ -119,50 +132,34 @@ public class SingboxUtil {
return false; return false;
} }
// 判断是否有正在运行的 singbox
public static boolean isSingBoxRunning(Context context) {
android.app.ActivityManager activityManager = (android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
java.util.List<android.app.ActivityManager.RunningAppProcessInfo> 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() { public static void stopSingBox() {
Process process = null; synchronized (SingboxUtil.class) { // 防止并发冲突
try { if (!isLibraryLoaded) {
// 检查是否支持 `pkill` 并停止 sing-box 进程 Log.e("SingBox", "Native library not loaded. Cannot perform StopVpn.");
ProcessBuilder builder = new ProcessBuilder("pkill", "-f", "sing-box"); return;
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);
} }
} catch (IOException e) {
Log.e("SingBox", "IOException occurred while trying to stop singbox", e); try {
} catch (InterruptedException e) { Log.d("SingBox", "Invoking StopVpn method on thread: " + Thread.currentThread().getName());
Log.e("SingBox", "The stopSingBox process was interrupted", e); int result = StopVpn();
Thread.currentThread().interrupt(); // 恢复中断状态 switch (result) {
} catch (Exception e) { case 0:
Log.e("SingBox", "Unexpected error occurred", e); Log.i("SingBox", "SingBox service stopped successfully using StopVpn.");
} finally { break;
// 确保关闭流以避免资源泄露 case -1:
if (process != null) { Log.e("SingBox", "Failed to stop SingBox: Instance is not initialized or already stopped.");
try { break;
process.getInputStream().close(); case -2:
process.getErrorStream().close(); Log.e("SingBox", "Failed to stop SingBox: Unexpected error occurred during the shutdown.");
} catch (IOException e) { break;
Log.e("SingBox", "Failed to close process streams", e); 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

@ -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();
}
}

View File

@ -0,0 +1,101 @@
/* Code generated by cmd/cgo; DO NOT EDIT. */
/* package command-line-arguments */
#line 1 "cgo-builtin-export-prolog"
#include <stddef.h>
#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 <string.h> // 声明 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 <complex.h>
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

Binary file not shown.

View File

@ -0,0 +1,7 @@
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:settingsActivity="com.example.MyAccessibilitySettingsActivity" />

View File

@ -1,4 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // 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 { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
} }

211
err.log
View File

@ -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-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-05-30 15:22:09.569 18506-18506 V2Ray com.example.studyapp E V2Ray files are missing, cannot start. 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.

View File

@ -19,4 +19,3 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }