From 3bde5cadb736ad0db37bc2028928352626944471 Mon Sep 17 00:00:00 2001 From: yjj38 Date: Mon, 9 Jun 2025 16:17:53 +0800 Subject: [PATCH] gffhfth --- .idea/codeStyles/Project.xml | 120 ++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/git_toolbox_prj.xml | 15 + app/src/main/AndroidManifest.xml | 3 +- .../com/example/studyapp/MainActivity.java | 403 +++++++----------- ...iceInfo.java => ChangeDeviceInfoUtil.java} | 13 +- .../studyapp/proxy/CustomVpnService.java | 339 --------------- .../com/example/studyapp/utils/ClashUtil.java | 133 ++++++ .../example/studyapp/utils/SingboxUtil.java | 166 -------- .../com/example/studyapp/utils/V2rayUtil.java | 180 -------- app/src/main/jniLibs/arm64-v8a/libnative.so | Bin 81472 -> 81472 bytes app/src/main/res/layout/activity_main.xml | 96 +++-- app/src/main/res/values/strings.xml | 5 +- ...g.xml => accessibility_service_config.xml} | 2 +- .../main/res/xml/network_security_config.xml | 3 +- 15 files changed, 496 insertions(+), 987 deletions(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/git_toolbox_prj.xml rename app/src/main/java/com/example/studyapp/device/{ChangeDeviceInfo.java => ChangeDeviceInfoUtil.java} (95%) delete mode 100644 app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java create mode 100644 app/src/main/java/com/example/studyapp/utils/ClashUtil.java delete mode 100644 app/src/main/java/com/example/studyapp/utils/SingboxUtil.java delete mode 100644 app/src/main/java/com/example/studyapp/utils/V2rayUtil.java rename app/src/main/res/xml/{AccessibilityServiceConfig.xml => accessibility_service_config.xml} (96%) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..57b2461 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,120 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..b9d18bf --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 955202d..00b7515 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,8 +54,7 @@ android:name=".service.MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:enabled="true" - android:exported="false" - android:foregroundServiceType="accessibility" > + android:exported="false"> diff --git a/app/src/main/java/com/example/studyapp/MainActivity.java b/app/src/main/java/com/example/studyapp/MainActivity.java index ff3625c..cf3f21f 100644 --- a/app/src/main/java/com/example/studyapp/MainActivity.java +++ b/app/src/main/java/com/example/studyapp/MainActivity.java @@ -1,10 +1,8 @@ package com.example.studyapp; import android.app.Activity; -import android.app.ActivityManager; import android.app.AlertDialog; import android.net.Uri; -import android.net.VpnService; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; @@ -12,10 +10,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.provider.Settings; -import android.util.Log; import android.widget.Button; import android.widget.Toast; import android.Manifest; @@ -30,278 +25,198 @@ 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.device.ChangeDeviceInfoUtil; +import com.example.studyapp.utils.ClashUtil; 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; - private static final int VPN_REQUEST_CODE = 100; // Adding the missing constant - private static final int ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE = 1001; + private static final int REQUEST_CODE_STORAGE_PERMISSION = 1; - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); + private static final int ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE = 1001; - 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) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions( - this, - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_CODE_STORAGE_PERMISSION - ); - } - } else { - // 针对 Android 11 及更高版本检查全文件管理权限 - if (!Environment.isExternalStorageManager()) { - // 请求权限 - Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivityForResult(intent, ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE); - } - } + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); - if (!isNetworkAvailable(this)) { - Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show(); - finish(); - } - // 初始化按钮 - Button runScriptButton = findViewById(R.id.run_script_button); - if (runScriptButton != null) { - runScriptButton.setOnClickListener(v -> AutoJsUtil.runAutojsScript(this)); - } else { - Toast.makeText(this, "Button not found", Toast.LENGTH_SHORT).show(); - } - - Button connectButton = findViewById(R.id.connectVpnButton); - if (connectButton != null) { - connectButton.setOnClickListener(v -> startProxyVpn(this)); - } else { - Toast.makeText(this, "Connect button not found", Toast.LENGTH_SHORT).show(); - } - - Button disconnectButton = findViewById(R.id.disconnectVpnButton); - if (disconnectButton != null) { - disconnectButton.setOnClickListener(v -> stopProxy(this)); - } else { - Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show(); - } - - Button modifyDeviceInfoButton = findViewById(R.id.modifyDeviceInfoButton); - if (modifyDeviceInfoButton != null) { - modifyDeviceInfoButton.setOnClickListener(v -> ChangeDeviceInfo.changeDeviceInfo(getPackageName(),this)); - } else { - Toast.makeText(this, "modifyDeviceInfo button not found", Toast.LENGTH_SHORT).show(); - } - - Button resetDeviceInfoButton = findViewById(R.id.resetDeviceInfoButton); - if (resetDeviceInfoButton != null) { - resetDeviceInfoButton.setOnClickListener(v -> ChangeDeviceInfo.resetChangedDeviceInfo(getPackageName(),this)); - } else { - 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); + 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) + != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_CODE_STORAGE_PERMISSION + ); + } + } else { + // 针对 Android 11 及更高版本检查全文件管理权限 + if (!Environment.isExternalStorageManager()) { + // 请求权限 + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivityForResult(intent, ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE); + } } - private void startProxyVpn(Context context) { - if (!isNetworkAvailable(context)) { - Toast.makeText(context, "Network is not available", Toast.LENGTH_SHORT).show(); - return; - } - - if (!(context instanceof Activity)) { - Toast.makeText(context, "Context must be an Activity", Toast.LENGTH_SHORT).show(); - return; - } - Activity activity = (Activity) context; - - try { - startProxyServer(activity); // 在主线程中调用 - } catch (IllegalStateException e) { - Toast.makeText(context, "Failed to start VPN: VPN Service illegal state", Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - Toast.makeText(context, "Failed to start VPN: " + (e.getMessage() != null ? e.getMessage() : "Unknown error"), Toast.LENGTH_SHORT).show(); - } + if (!isNetworkAvailable(this)) { + Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show(); + finish(); } - 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); - } + PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(CheckAccessibilityWorker.class, 15, TimeUnit.MINUTES) + .build(); + WorkManager.getInstance(this).enqueue(workRequest); + + // 初始化按钮 + Button runScriptButton = findViewById(R.id.run_script_button); + if (runScriptButton != null) { + runScriptButton.setOnClickListener(v -> AutoJsUtil.runAutojsScript(this)); + } else { + Toast.makeText(this, "Button not found", Toast.LENGTH_SHORT).show(); } - private void showToastOnUiThread(Context context, String message) { - new Handler(Looper.getMainLooper()).post(() -> - Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); + Button connectButton = findViewById(R.id.connectVpnButton); + if (connectButton != null) { + connectButton.setOnClickListener(v -> startProxyVpn(this)); + } else { + Toast.makeText(this, "Connect button not found", Toast.LENGTH_SHORT).show(); } - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, "Storage Permissions granted", Toast.LENGTH_SHORT).show(); - } else { - // 提示权限被拒绝,同时允许用户重新授予权限 - showPermissionExplanationDialog(); - } - } + Button disconnectButton = findViewById(R.id.disconnectVpnButton); + if (disconnectButton != null) { + disconnectButton.setOnClickListener(v -> ClashUtil.stopProxy(this)); + } else { + Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show(); } - - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE: - handleStoragePermissionResult(resultCode); - break; - case VPN_REQUEST_CODE: - handleVpnPermissionResult(resultCode); - break; - default: - break; - } + Button switchVpnButton = findViewById(R.id.switchVpnButton); + if (switchVpnButton != null) { + switchVpnButton.setOnClickListener(v -> ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170")); + } else { + Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show(); } - private void handleStoragePermissionResult(int resultCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) { - Toast.makeText(this, "Storage Permissions granted", Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(this, "请授予所有文件管理权限", Toast.LENGTH_SHORT).show(); - finish(); - } + Button modifyDeviceInfoButton = findViewById(R.id.modifyDeviceInfoButton); + if (modifyDeviceInfoButton != null) { + modifyDeviceInfoButton.setOnClickListener(v -> ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170")); + } else { + Toast.makeText(this, "modifyDeviceInfo button not found", Toast.LENGTH_SHORT).show(); } - 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; + Button resetDeviceInfoButton = findViewById(R.id.resetDeviceInfoButton); + if (resetDeviceInfoButton != null) { + resetDeviceInfoButton.setOnClickListener(v -> ChangeDeviceInfoUtil.resetChangedDeviceInfo(getPackageName(), this)); + } else { + Toast.makeText(this, "resetDeviceInfo button not found", Toast.LENGTH_SHORT).show(); } - private void stopProxy(Context context) { - if (context == null) { - Log.e("stopProxy", "上下文为空,无法停止服务"); - return; - } + // try { + // if (!ClashUtil.checkProxy(this)) { + // 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); + // } + } - 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 void startProxyVpn(Context context) { + if (!isNetworkAvailable(context)) { + Toast.makeText(context, "Network is not available", Toast.LENGTH_SHORT).show(); + return; } - 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); - } + if (!(context instanceof Activity)) { + Toast.makeText(context, "Context must be an Activity", Toast.LENGTH_SHORT).show(); + return; } + Activity activity = (Activity) context; - private void showPermissionExplanationDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Permission Required") - .setMessage("Storage Permission is required for the app to function. Please enable it in Settings.") - .setPositiveButton("Go to Settings", (dialog, which) -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", getPackageName(), null); - intent.setData(uri); - startActivity(intent); - }) - .setNegativeButton("Cancel", (dialog, which) -> finish()) - .show(); + try { + ClashUtil.startProxy(context); // 在主线程中调用 + } catch (IllegalStateException e) { + Toast.makeText(context, "Failed to start VPN: VPN Service illegal state", Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + Toast.makeText(context, "Failed to start VPN: " + (e.getMessage() != null ? e.getMessage() : "Unknown error"), Toast.LENGTH_SHORT).show(); } + } - @Override - protected void onDestroy() { - super.onDestroy(); - if (AutoJsUtil.scriptResultReceiver != null) { - unregisterReceiver(AutoJsUtil.scriptResultReceiver); - } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "Storage Permissions granted", Toast.LENGTH_SHORT).show(); + } else { + // 提示权限被拒绝,同时允许用户重新授予权限 + showPermissionExplanationDialog(); + } } + } - private boolean isNetworkAvailable(Context context) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivityManager != null) { - Network network = connectivityManager.getActiveNetwork(); - if (network != null) { - NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); - return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - } - } - return false; + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + switch (requestCode) { + case ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE: + handleStoragePermissionResult(resultCode); + break; + default: + break; } + } + + private void handleStoragePermissionResult(int resultCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) { + Toast.makeText(this, "Storage Permissions granted", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "请授予所有文件管理权限", Toast.LENGTH_SHORT).show(); + finish(); + } + } + + private void showPermissionExplanationDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Permission Required") + .setMessage("Storage Permission is required for the app to function. Please enable it in Settings.") + .setPositiveButton("Go to Settings", (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + }) + .setNegativeButton("Cancel", (dialog, which) -> finish()) + .show(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (AutoJsUtil.scriptResultReceiver != null) { + unregisterReceiver(AutoJsUtil.scriptResultReceiver); + } + } + + private boolean isNetworkAvailable(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager != null) { + Network network = connectivityManager.getActiveNetwork(); + if (network != null) { + NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); + return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } + } + return false; + } } diff --git a/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java b/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfoUtil.java similarity index 95% rename from app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java rename to app/src/main/java/com/example/studyapp/device/ChangeDeviceInfoUtil.java index 114f7c1..91298ee 100644 --- a/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java +++ b/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfoUtil.java @@ -1,16 +1,9 @@ package com.example.studyapp.device; -import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; -import android.net.ConnectivityManager; -import android.net.ProxyInfo; 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 org.json.JSONObject; @@ -18,7 +11,7 @@ import org.json.JSONObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class ChangeDeviceInfo { +public class ChangeDeviceInfoUtil { public static void changeDeviceInfo(String current_pkg_name,Context context) { // 指定包名优先级高于全局 @@ -81,12 +74,12 @@ public class ChangeDeviceInfo { } 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); } 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; } diff --git a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java b/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java deleted file mode 100644 index 405e9bc..0000000 --- a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java +++ /dev/null @@ -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 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 getSystemDnsServers() { - List 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; - } -} diff --git a/app/src/main/java/com/example/studyapp/utils/ClashUtil.java b/app/src/main/java/com/example/studyapp/utils/ClashUtil.java new file mode 100644 index 0000000..2d4e4ac --- /dev/null +++ b/app/src/main/java/com/example/studyapp/utils/ClashUtil.java @@ -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(); + } + } + }); + } + +} diff --git a/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java b/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java deleted file mode 100644 index 705a32d..0000000 --- a/app/src/main/java/com/example/studyapp/utils/SingboxUtil.java +++ /dev/null @@ -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); - } - } - } -} diff --git a/app/src/main/java/com/example/studyapp/utils/V2rayUtil.java b/app/src/main/java/com/example/studyapp/utils/V2rayUtil.java deleted file mode 100644 index f7136c8..0000000 --- a/app/src/main/java/com/example/studyapp/utils/V2rayUtil.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/app/src/main/jniLibs/arm64-v8a/libnative.so b/app/src/main/jniLibs/arm64-v8a/libnative.so index 6bebff15d5339829b7bdda30c359c43a69b81056..f87c13de1240a22c618468eae663df69a1ad1ed9 100644 GIT binary patch delta 13542 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 - + -