diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 33b91aa..b268ef3 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -5,9 +5,6 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c11b271..5d452b6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
-
+
+
diff --git a/app/src/main/java/com/example/studyapp/MainActivity.java b/app/src/main/java/com/example/studyapp/MainActivity.java
index 5571d86..ad4cbcf 100644
--- a/app/src/main/java/com/example/studyapp/MainActivity.java
+++ b/app/src/main/java/com/example/studyapp/MainActivity.java
@@ -21,9 +21,12 @@ import androidx.appcompat.app.AppCompatActivity;
import com.example.studyapp.request.ScriptResultRequest;
import com.example.studyapp.service.CloudPhoneManageService;
+import com.example.studyapp.socks.SingBoxLauncher;
+import com.example.studyapp.utils.IpUtil;
import com.example.studyapp.utils.ShellUtils;
import java.io.File;
+import java.lang.ref.WeakReference;
import retrofit2.Call;
import retrofit2.Callback;
@@ -43,23 +46,6 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
- vpnRequestLauncher = registerForActivityResult(
- new ActivityResultContracts.StartActivityForResult(),
- result -> {
- if (result.getResultCode() == RESULT_OK) {
- startProxyVpnService();
- } else {
- Toast.makeText(this, "VPN setup failed", Toast.LENGTH_SHORT).show();
- new AlertDialog.Builder(this)
- .setTitle("VPN 配置失败")
- .setMessage("未能成功配置 VPN。要重试吗?")
- .setPositiveButton("重试", (dialog, which) -> startProxyVpn(this))
- .setNegativeButton("取消", null)
- .show();
- }
- }
- );
-
// 检查存储权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
@@ -68,7 +54,6 @@ public class MainActivity extends AppCompatActivity {
REQUEST_CODE_STORAGE_PERMISSION);
}
- startProxyVpn(this);
// 查找按钮对象
Button runScriptButton = findViewById(R.id.run_script_button);
@@ -79,18 +64,20 @@ public class MainActivity extends AppCompatActivity {
}
}
- private void startProxyVpn(Context context) { // 避免强制依赖 MainActivity
- Intent intent = VpnService.prepare(context);
- if (intent != null) {
- vpnRequestLauncher.launch(intent);
- } else {
- startProxyVpnService();
- }
- }
-
- private void startProxyVpnService() {
- Intent serviceIntent = new Intent(this, ProxyVpnService.class);
- startService(serviceIntent);
+ private void startProxyVpn(Context context) {
+ WeakReference contextRef = new WeakReference<>(context);
+ new Thread(() -> {
+ try {
+ SingBoxLauncher.getInstance().start(context, IpUtil.safeClientIp());
+ } catch (Exception e) {
+ Context ctx = contextRef.get();
+ if (ctx != null) {
+ runOnUiThread(() ->
+ Toast.makeText(ctx, "Failed to start VPN: " + e.getMessage(), Toast.LENGTH_SHORT).show()
+ );
+ }
+ }
+ }).start();
}
@Override
@@ -98,8 +85,8 @@ public class MainActivity extends AppCompatActivity {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- permissionHandler();
Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show();
+ startProxyVpn(this);
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
// 可选择终止操作或退出程序
@@ -219,23 +206,4 @@ public class MainActivity extends AppCompatActivity {
}
});
}
-
- private void grantPermission(String name)
- {
- ShellUtils.execRootCmd("pm grant " + this.getPackageName() + " " + name);
- }
- private void appopsAllow(String name)
- {
- ShellUtils.execRootCmd("appops set " + this.getPackageName() + " " + name + " allow");
- }
- private void permissionHandler() {
- grantPermission("android.permission.SYSTEM_ALERT_WINDOW");
- grantPermission("android.permission.FOREGROUND_SERVICE");
- grantPermission("android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION");
- grantPermission("android.permission.WRITE_SECURE_SETTINGS");
- grantPermission("android.permission.READ_EXTERNAL_STORAGE");
- grantPermission("android.permission.WRITE_EXTERNAL_STORAGE");
- appopsAllow("MANAGE_EXTERNAL_STORAGE");
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/studyapp/ProxyVpnService.java b/app/src/main/java/com/example/studyapp/ProxyVpnService.java
deleted file mode 100644
index 02b8573..0000000
--- a/app/src/main/java/com/example/studyapp/ProxyVpnService.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.example.studyapp;
-
-import android.content.Intent;
-import android.net.VpnService;
-import android.os.ParcelFileDescriptor;
-
-import java.io.IOException;
-
-public class ProxyVpnService extends VpnService {
- private ParcelFileDescriptor vpnInterface;
-
- @Override
- public void onCreate() {
- super.onCreate();
- Builder builder = new Builder();
- try {
- builder.addAddress("10.0.2.15", 24); // 配置虛擬 IP
- builder.addRoute("0.0.0.0", 0); // 配置攔截所有流量的路由
- builder.setSession("Proxy VPN Service");
- builder.addDnsServer("8.8.8.8"); // 設置 DNS
- vpnInterface = builder.establish(); // 啟動 VPN 通道,保存接口描述符
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (vpnInterface != null) {
- try {
- vpnInterface.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- // 在实际实现中可能需要处理 Intent 数据
- return START_STICKY;
- }
-}
diff --git a/app/src/main/java/com/example/studyapp/socks/SingBoxLauncher.java b/app/src/main/java/com/example/studyapp/socks/SingBoxLauncher.java
new file mode 100644
index 0000000..cf8c66a
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/socks/SingBoxLauncher.java
@@ -0,0 +1,304 @@
+package com.example.studyapp.socks;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import com.example.studyapp.utils.ShellUtils;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+public class SingBoxLauncher {
+ private static SingBoxLauncher instance;
+ private static final String PKG = "io.nekohasekai.sfa";
+
+ private final CountDownLatch latch;
+
+ public static SingBoxLauncher getInstance() {
+ if (instance == null) {
+ synchronized (SingBoxLauncher.class) {
+ if (instance == null) {
+ instance = new SingBoxLauncher();
+ }
+ }
+ }
+ return instance;
+ }
+
+ private final HandlerThread handlerThread;
+
+ private SingBoxLauncher() {
+ latch = new CountDownLatch(1);
+ handlerThread = new HandlerThread("SingBoxThread");
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper());
+ }
+
+ public void shutdown() {
+ handlerThread.quitSafely();
+ }
+
+ private void checkVPNApp(int count) {
+ if (count > 3) {
+ Log.e("checkVPNApp", "Invalid count parameter: " + count);
+ return;
+ }
+
+ while (count <= 3) {
+ try {
+ if (!isProcessRunning(PKG)) {
+ startApp(PKG);
+ } else {
+ break;
+ }
+ // 避免 Thread.sleep,改用异步任务定时调度
+ TimeUnit.SECONDS.sleep(2);
+ count++;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // 恢复中断状态
+ Log.e("checkVPNApp", "Thread interrupted: " + e.getMessage());
+ break;
+ }
+ }
+ }
+
+ // 工具方法
+ private boolean isProcessRunning(String pkg) {
+ String command = "ps -A | grep " + sanitizePackageName(pkg);
+ String result = ShellUtils.execRootCmdAndGetResult(command);
+ return result != null && !result.isEmpty();
+ }
+
+ private void startApp(String pkg) {
+ ShellUtils.execRootCmd("am start -n " + sanitizePackageName(pkg) + "/.ui.MainActivity");
+ }
+
+ private String sanitizePackageName(String pkg) {
+ if (pkg.matches("^[a-zA-Z0-9._-]+$")) {
+ return pkg;
+ } else {
+ throw new IllegalArgumentException("Unsafe package name: " + pkg);
+ }
+ }
+
+ public boolean start(Context context,String originIp) {
+ checkVPNApp(0);
+
+ Future> future = executorService.submit(() -> {
+
+ String content = "{\n" +
+ " \"dns\": {\n" +
+ " \"independent_cache\": true,\n" +
+ " \"servers\": [\n" +
+ " {\n" +
+ " \"tag\": \"cloudflare\",\n" +
+ " \"address\": \"tls://1.1.1.1\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"tag\": \"local\",\n" +
+ " \"address\": \"tls://1.1.1.1\",\n" +
+ " \"detour\": \"direct\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"tag\": \"remote\",\n" +
+ " \"address\": \"fakeip\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"server\": \"local\",\n" +
+ " \"outbound\": \"any\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"server\": \"remote\",\n" +
+ " \"query_type\": [\n" +
+ " \"A\",\n" +
+ " \"AAAA\"\n" +
+ " ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"fakeip\": {\n" +
+ " \"enabled\": true,\n" +
+ " \"inet4_range\": \"198.18.0.0/15\",\n" +
+ " \"inet6_range\": \"fc00::/18\"\n" +
+ " },\n" +
+ " \"final\": \"cloudflare\"\n" +
+ " },\n" +
+ " \"inbounds\": [\n" +
+ " {\n" +
+ " \"type\": \"tun\",\n" +
+ " \"tag\": \"tun-in\",\n" +
+ " \"address\": [\n" +
+ " \"172.19.0.1/28\"\n" +
+ " ],\n" +
+ " \"auto_route\": true,\n" +
+ " \"sniff\": true,\n" +
+ " \"strict_route\": false,\n" +
+ " \"domain_strategy\": \"ipv4_only\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"log\": {\n" +
+ " \"level\": \"trace\"\n" +
+ " },\n" +
+ " \"outbounds\": [\n" +
+ " {\n" +
+ " \"type\": \"socks\",\n" +
+ " \"tag\": \"proxy\",\n" +
+ " \"version\": \"5\",\n" +
+ " \"network\": \"tcp\",\n" +
+ " \"udp_over_tcp\": {\n" +
+ " \"enabled\": true\n" +
+ " },\n" +
+ " \"username\": \"cut_team_protoc_vast-zone-custom-region-us\",\n" +
+ " \"password\": \"Leoliu811001\",\n" +
+ " \"server\": \"105bd58a50330382.na.ipidea.online\",\n" +
+ " \"server_port\": 2333\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"dns\",\n" +
+ " \"tag\": \"dns-out\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"direct\",\n" +
+ " \"tag\": \"direct\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"block\",\n" +
+ " \"tag\": \"block\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"route\": {\n" +
+ " \"final\": \"proxy\",\n" +
+ " \"auto_detect_interface\": true,\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"protocol\": \"dns\",\n" +
+ " \"outbound\": \"dns-out\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"protocol\": [\n" +
+ " \"stun\",\n" +
+ " \"quic\"\n" +
+ " ],\n" +
+ " \"outbound\": \"block\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"ip_is_private\": true,\n" +
+ " \"outbound\": \"direct\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"ip_cidr\": \"\",\n" +
+ " \"outbound\": \"direct\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"domain\": \"cpm-api.resi-prod.resi-oversea.com\",\n" +
+ " \"domain_suffix\": \"resi-oversea.com\",\n" +
+ " \"outbound\": \"direct\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "}";
+
+ Intent intent = new Intent();
+ intent.setAction(PKG + ".action.START_VPN");
+ intent.putExtra("content", content);
+ intent.putExtra("originIp", originIp);
+ context.sendBroadcast(intent);
+
+ handler.postDelayed(() -> checkVPN(context, 0), 1000);
+ });
+
+ try {
+ future.get(); // 等待任务完成,捕获异常
+ } catch (Exception e) {
+ Log.e("TaskError", "Task failed to execute properly", e);
+ }
+
+ try {
+ boolean result = latch.await(5, TimeUnit.SECONDS); // 设置超时时间
+ if (!result) {
+ isVpnRunning = false; // 超时回退逻辑
+ }
+ } catch (InterruptedException ignored) {
+ isVpnRunning = false;
+ }
+ return isVpnRunning;
+ }
+
+ public void stop(Context context) {
+ if (context == null || PKG == null || PKG.trim().isEmpty()) {
+ Log.e("SingBoxLauncher", "Invalid context or package name.");
+ return;
+ }
+
+ try {
+ // 构建广播
+ Intent intent = new Intent();
+ intent.setAction(PKG + ".action.STOP_VPN");
+ intent.setPackage(PKG); // 安全性增强,限制接收者
+ context.sendBroadcast(intent);
+
+ // 检查进程是否仍在运行
+ if (isProcessRunning(PKG)) {
+ // 添加延迟等待,确保广播完成
+ Thread.sleep(1000);
+
+ // 执行强制停止命令
+ ShellUtils.execRootCmd("am force-stop " + sanitizePackageName(PKG));
+ }
+ } catch (Exception e) {
+ Log.e("SingBoxLauncher", "Error while stopping VPN app: " + e.getMessage());
+ }
+ }
+
+ private void checkVPN(Context context, int count) {
+ try {
+ if (checkVPN(context)) {
+ isVpnRunning = true;
+ Log.d("VPNCheck", "VPN is running.");
+ latch.countDown();
+ } else {
+ Log.d("VPNCheck", "VPN is not running. Retry count: " + count);
+ if (count > 3) {
+ isVpnRunning = false;
+ latch.countDown();
+ } else {
+ final int countFinal = count + 1;
+ handler.removeCallbacksAndMessages(null); // 清理旧任务
+ handler.postDelayed(() -> checkVPN(context, countFinal), 1000);
+ }
+ }
+ } catch (Exception e) {
+ Log.e("VPNCheck", "Error in checkVPN", e);
+ isVpnRunning = false;
+ latch.countDown();
+ }
+ }
+ // 检查网络中的 VPN 连接
+ private boolean checkVPN(Context context) {
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivityManager == null) return false;
+
+ android.net.Network activeNetwork = connectivityManager.getActiveNetwork();
+ if (activeNetwork == null) return false;
+
+ android.net.NetworkCapabilities networkCapabilities =
+ connectivityManager.getNetworkCapabilities(activeNetwork);
+ if (networkCapabilities == null) return false;
+
+ return networkCapabilities.hasTransport(android.net.NetworkCapabilities.TRANSPORT_VPN);
+ }
+
+ private boolean isVpnRunning;
+ private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+ private final Handler handler;
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/HttpUtil.java b/app/src/main/java/com/example/studyapp/utils/HttpUtil.java
new file mode 100644
index 0000000..cd1bf37
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/utils/HttpUtil.java
@@ -0,0 +1,59 @@
+package com.example.studyapp.utils;
+
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class HttpUtil {
+ private static final String TAG = "HttpUtil";
+
+ public static String requestGet(String url) throws IOException {
+ Log.d(TAG, "[requestGet][url=" + url + "]");
+ HttpURLConnection connection = (HttpURLConnection)new URL(url).openConnection();
+ /*connection.setRequestProperty("Connection", "keep-alive");
+ connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
+ connection.setRequestProperty("X-Requested-With", "com.android.chrome");
+ connection.setRequestProperty("Sec-Fetch-Mode", "navigate");
+ connection.setRequestProperty("Sec-Fetch-User", "?1");
+ connection.setRequestProperty("Sec-Fetch-Dest", "document");
+ connection.setRequestProperty("Sec-Fetch-Site", "none");
+ connection.setRequestProperty("Sec-Ch-Ua-Mobile", "?1");
+ connection.setRequestProperty("Sec-Ch-Ua-Platform", "Android");
+ connection.setRequestProperty("Upgrade-Insecure-Requests", "1");
+ connection.setInstanceFollowRedirects(true);*/
+ connection.setDoInput(true);
+ connection.setConnectTimeout(15000);
+ connection.setReadTimeout(15000);
+ connection.setRequestMethod("GET");
+ connection.setUseCaches(false);
+ connection.connect();
+
+ String responseBody = readResponseBody(connection);
+ Log.d(TAG, "[requestGet][response=" + responseBody + "]");
+ connection.disconnect();
+
+ return responseBody;
+ }
+
+
+
+ private static String readResponseBody(HttpURLConnection connection) throws IOException {
+ InputStream inputStream = connection.getInputStream();
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[0x400];
+ while(true) {
+ int l = inputStream.read(buffer);
+ if(l == -1) {
+ break;
+ }
+
+ byteArrayOutputStream.write(buffer, 0, l);
+ }
+
+ return byteArrayOutputStream.toString();
+ }
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/IpUtil.java b/app/src/main/java/com/example/studyapp/utils/IpUtil.java
new file mode 100644
index 0000000..aac7a0b
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/utils/IpUtil.java
@@ -0,0 +1,67 @@
+package com.example.studyapp.utils;
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Pattern;
+
+public class IpUtil {
+ public static boolean isValidIPAddress(String ipAddress) {
+ if ((ipAddress != null) && (!ipAddress.isEmpty()))
+ {
+ return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress);
+ }
+ return false;
+ }
+ public static String getClientIp(String url) {
+ try {
+ String s = HttpUtil.requestGet(url);
+ if (!TextUtils.isEmpty(s) && isValidIPAddress(s))
+ {
+ return s;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+ public static String getClientIp() {
+ return getClientIp("http://47.236.153.142/");
+ }
+
+ public static String safeClientIp() {
+ String result = getClientIp("http://47.236.153.142/");//SG
+ if (TextUtils.isEmpty(result))
+ {
+ result = getClientIp("http://8.211.204.20/");//UK
+ }
+ return result;
+ }
+
+ public static String checkClientIp(String excludeCountry)
+ {
+ try {
+ String s = HttpUtil.requestGet("https://get.geojs.io/v1/ip/country.json");
+ if (!TextUtils.isEmpty(s))
+ {
+ JSONObject json = new JSONObject(s);
+ if (excludeCountry.equalsIgnoreCase(json.getString("country")))
+ {
+ return "";
+ }
+ return json.getString("ip");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+
+ }
+
+ public static CompletableFuture getClientIpFuture()
+ {
+ return CompletableFuture.supplyAsync(IpUtil::safeClientIp);
+ }
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/ShellUtils.java b/app/src/main/java/com/example/studyapp/utils/ShellUtils.java
index 23c6387..a7fd258 100644
--- a/app/src/main/java/com/example/studyapp/utils/ShellUtils.java
+++ b/app/src/main/java/com/example/studyapp/utils/ShellUtils.java
@@ -54,10 +54,10 @@ public class ShellUtils {
return false;
}
- public static String execRootCmdAndGetResult(String cmd) {
+ public static String execRootCmdAndGetResult(String cmd){
if (cmd == null || cmd.trim().isEmpty() || !isCommandSafe(cmd)) {
Log.e("ShellUtils", "Unsafe or empty command. Aborting execution.");
- return null;
+ throw new IllegalArgumentException("Unsafe or empty command.");
}
Process process = null;
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index cc4e4cc..23d75da 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -16,5 +16,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击开始运行"
+ android:contentDescription="运行脚本按钮"
android:layout_centerInParent="true"/>
\ No newline at end of file