Refactor VPN implementation and add SingBoxLauncher integration
Replaced ProxyVpnService with SingBoxLauncher for enhanced VPN control and functionality. Introduced new utilities such as IpUtil and HttpUtil for network-related operations. Updated MainActivity to utilize the new VPN workflow and removed outdated VPN service logic.
This commit is contained in:
parent
c93475533d
commit
9e64a65d62
|
@ -5,9 +5,6 @@
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
<SelectionState runConfigName="Vpn">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -1,7 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||||
|
|
|
@ -21,9 +21,12 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import com.example.studyapp.request.ScriptResultRequest;
|
import com.example.studyapp.request.ScriptResultRequest;
|
||||||
import com.example.studyapp.service.CloudPhoneManageService;
|
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 com.example.studyapp.utils.ShellUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
|
@ -43,23 +46,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
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)
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
@ -68,7 +54,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
REQUEST_CODE_STORAGE_PERMISSION);
|
REQUEST_CODE_STORAGE_PERMISSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
startProxyVpn(this);
|
|
||||||
|
|
||||||
// 查找按钮对象
|
// 查找按钮对象
|
||||||
Button runScriptButton = findViewById(R.id.run_script_button);
|
Button runScriptButton = findViewById(R.id.run_script_button);
|
||||||
|
@ -79,18 +64,20 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startProxyVpn(Context context) { // 避免强制依赖 MainActivity
|
private void startProxyVpn(Context context) {
|
||||||
Intent intent = VpnService.prepare(context);
|
WeakReference<Context> contextRef = new WeakReference<>(context);
|
||||||
if (intent != null) {
|
new Thread(() -> {
|
||||||
vpnRequestLauncher.launch(intent);
|
try {
|
||||||
} else {
|
SingBoxLauncher.getInstance().start(context, IpUtil.safeClientIp());
|
||||||
startProxyVpnService();
|
} catch (Exception e) {
|
||||||
}
|
Context ctx = contextRef.get();
|
||||||
}
|
if (ctx != null) {
|
||||||
|
runOnUiThread(() ->
|
||||||
private void startProxyVpnService() {
|
Toast.makeText(ctx, "Failed to start VPN: " + e.getMessage(), Toast.LENGTH_SHORT).show()
|
||||||
Intent serviceIntent = new Intent(this, ProxyVpnService.class);
|
);
|
||||||
startService(serviceIntent);
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -98,8 +85,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
|
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
permissionHandler();
|
|
||||||
Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show();
|
||||||
|
startProxyVpn(this);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> getClientIpFuture()
|
||||||
|
{
|
||||||
|
return CompletableFuture.supplyAsync(IpUtil::safeClientIp);
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,10 +54,10 @@ public class ShellUtils {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String execRootCmdAndGetResult(String cmd) {
|
public static String execRootCmdAndGetResult(String cmd){
|
||||||
if (cmd == null || cmd.trim().isEmpty() || !isCommandSafe(cmd)) {
|
if (cmd == null || cmd.trim().isEmpty() || !isCommandSafe(cmd)) {
|
||||||
Log.e("ShellUtils", "Unsafe or empty command. Aborting execution.");
|
Log.e("ShellUtils", "Unsafe or empty command. Aborting execution.");
|
||||||
return null;
|
throw new IllegalArgumentException("Unsafe or empty command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Process process = null;
|
Process process = null;
|
||||||
|
|
|
@ -16,5 +16,6 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="点击开始运行"
|
android:text="点击开始运行"
|
||||||
|
android:contentDescription="运行脚本按钮"
|
||||||
android:layout_centerInParent="true"/>
|
android:layout_centerInParent="true"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
Loading…
Reference in New Issue