gffhfth
This commit is contained in:
parent
c20f4cb515
commit
3bde5cadb7
|
@ -0,0 +1,120 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<ScalaCodeStyleSettings>
|
||||||
|
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
||||||
|
</ScalaCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="GoogleStyle" />
|
||||||
|
</state>
|
||||||
|
</component>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitToolBoxProjectSettings">
|
||||||
|
<option name="commitMessageIssueKeyValidationOverride">
|
||||||
|
<BoolValueOverride>
|
||||||
|
<option name="enabled" value="true" />
|
||||||
|
</BoolValueOverride>
|
||||||
|
</option>
|
||||||
|
<option name="commitMessageValidationEnabledOverride">
|
||||||
|
<BoolValueOverride>
|
||||||
|
<option name="enabled" value="true" />
|
||||||
|
</BoolValueOverride>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -54,8 +54,7 @@
|
||||||
android:name=".service.MyAccessibilityService"
|
android:name=".service.MyAccessibilityService"
|
||||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false">
|
||||||
android:foregroundServiceType="accessibility" >
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package com.example.studyapp;
|
package com.example.studyapp;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.net.VpnService;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
@ -12,10 +10,7 @@ import android.net.Network;
|
||||||
import android.net.NetworkCapabilities;
|
import android.net.NetworkCapabilities;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
@ -30,278 +25,198 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.work.PeriodicWorkRequest;
|
import androidx.work.PeriodicWorkRequest;
|
||||||
import androidx.work.WorkManager;
|
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.ChangeDeviceInfoUtil;
|
||||||
import com.example.studyapp.proxy.CustomVpnService;
|
|
||||||
import com.example.studyapp.utils.ReflectionHelper;
|
|
||||||
|
|
||||||
|
import com.example.studyapp.utils.ClashUtil;
|
||||||
import com.example.studyapp.worker.CheckAccessibilityWorker;
|
import com.example.studyapp.worker.CheckAccessibilityWorker;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
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 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
|
private static final int ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE = 1001;
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
|
|
||||||
System.setProperty("java.library.path", this.getApplicationInfo().nativeLibraryDir);
|
@Override
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
// 针对 Android 10 或更低版本检查普通存储权限
|
super.onCreate(savedInstanceState);
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
setContentView(R.layout.activity_main);
|
||||||
!= 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNetworkAvailable(this)) {
|
System.setProperty("java.library.path", this.getApplicationInfo().nativeLibraryDir);
|
||||||
Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
finish();
|
// 针对 Android 10 或更低版本检查普通存储权限
|
||||||
}
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
// 初始化按钮
|
!= PackageManager.PERMISSION_GRANTED
|
||||||
Button runScriptButton = findViewById(R.id.run_script_button);
|
) {
|
||||||
if (runScriptButton != null) {
|
ActivityCompat.requestPermissions(
|
||||||
runScriptButton.setOnClickListener(v -> AutoJsUtil.runAutojsScript(this));
|
this,
|
||||||
} else {
|
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||||
Toast.makeText(this, "Button not found", Toast.LENGTH_SHORT).show();
|
REQUEST_CODE_STORAGE_PERMISSION
|
||||||
}
|
);
|
||||||
|
}
|
||||||
Button connectButton = findViewById(R.id.connectVpnButton);
|
} else {
|
||||||
if (connectButton != null) {
|
// 针对 Android 11 及更高版本检查全文件管理权限
|
||||||
connectButton.setOnClickListener(v -> startProxyVpn(this));
|
if (!Environment.isExternalStorageManager()) {
|
||||||
} else {
|
// 请求权限
|
||||||
Toast.makeText(this, "Connect button not found", Toast.LENGTH_SHORT).show();
|
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);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startProxyVpn(Context context) {
|
if (!isNetworkAvailable(this)) {
|
||||||
if (!isNetworkAvailable(context)) {
|
Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
|
||||||
Toast.makeText(context, "Network is not available", Toast.LENGTH_SHORT).show();
|
finish();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startProxyServer(Activity activity) {
|
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(CheckAccessibilityWorker.class, 15, TimeUnit.MINUTES)
|
||||||
// 请求 VPN 权限
|
.build();
|
||||||
Intent vpnPrepareIntent = VpnService.prepare(activity);
|
WorkManager.getInstance(this).enqueue(workRequest);
|
||||||
if (vpnPrepareIntent != null) {
|
|
||||||
// 如果尚未授予权限,请求权限,等待结果回调
|
// 初始化按钮
|
||||||
startActivityForResult(vpnPrepareIntent, VPN_REQUEST_CODE);
|
Button runScriptButton = findViewById(R.id.run_script_button);
|
||||||
} else {
|
if (runScriptButton != null) {
|
||||||
// 如果已经授予权限,直接调用 onActivityResult 模拟结果处理
|
runScriptButton.setOnClickListener(v -> AutoJsUtil.runAutojsScript(this));
|
||||||
onActivityResult(VPN_REQUEST_CODE, RESULT_OK, null);
|
} else {
|
||||||
}
|
Toast.makeText(this, "Button not found", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showToastOnUiThread(Context context, String message) {
|
Button connectButton = findViewById(R.id.connectVpnButton);
|
||||||
new Handler(Looper.getMainLooper()).post(() ->
|
if (connectButton != null) {
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show());
|
connectButton.setOnClickListener(v -> startProxyVpn(this));
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Connect button not found", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
Button disconnectButton = findViewById(R.id.disconnectVpnButton);
|
||||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
if (disconnectButton != null) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
disconnectButton.setOnClickListener(v -> ClashUtil.stopProxy(this));
|
||||||
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
|
} else {
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show();
|
||||||
Toast.makeText(this, "Storage Permissions granted", Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
// 提示权限被拒绝,同时允许用户重新授予权限
|
|
||||||
showPermissionExplanationDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button switchVpnButton = findViewById(R.id.switchVpnButton);
|
||||||
@Override
|
if (switchVpnButton != null) {
|
||||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
switchVpnButton.setOnClickListener(v -> ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170"));
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
} else {
|
||||||
|
Toast.makeText(this, "Disconnect button not found", Toast.LENGTH_SHORT).show();
|
||||||
switch (requestCode) {
|
|
||||||
case ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE:
|
|
||||||
handleStoragePermissionResult(resultCode);
|
|
||||||
break;
|
|
||||||
case VPN_REQUEST_CODE:
|
|
||||||
handleVpnPermissionResult(resultCode);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleStoragePermissionResult(int resultCode) {
|
Button modifyDeviceInfoButton = findViewById(R.id.modifyDeviceInfoButton);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
|
if (modifyDeviceInfoButton != null) {
|
||||||
Toast.makeText(this, "Storage Permissions granted", Toast.LENGTH_SHORT).show();
|
modifyDeviceInfoButton.setOnClickListener(v -> ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170"));
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "请授予所有文件管理权限", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "modifyDeviceInfo button not found", Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isServiceRunning(Context context, Class<?> serviceClass) {
|
Button resetDeviceInfoButton = findViewById(R.id.resetDeviceInfoButton);
|
||||||
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
if (resetDeviceInfoButton != null) {
|
||||||
if (manager != null) {
|
resetDeviceInfoButton.setOnClickListener(v -> ChangeDeviceInfoUtil.resetChangedDeviceInfo(getPackageName(), this));
|
||||||
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
|
} else {
|
||||||
if (serviceClass.getName().equals(service.service.getClassName())) {
|
Toast.makeText(this, "resetDeviceInfo button not found", Toast.LENGTH_SHORT).show();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopProxy(Context context) {
|
// try {
|
||||||
if (context == null) {
|
// if (!ClashUtil.checkProxy(this)) {
|
||||||
Log.e("stopProxy", "上下文为空,无法停止服务");
|
// startProxyVpn(this);
|
||||||
return;
|
// }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)) {
|
private void startProxyVpn(Context context) {
|
||||||
Log.w("stopProxy", "服务未运行,无法停止");
|
if (!isNetworkAvailable(context)) {
|
||||||
return;
|
Toast.makeText(context, "Network is not available", Toast.LENGTH_SHORT).show();
|
||||||
}
|
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 Intent intent;
|
if (!(context instanceof Activity)) {
|
||||||
private void handleVpnPermissionResult(int resultCode) {
|
Toast.makeText(context, "Context must be an Activity", Toast.LENGTH_SHORT).show();
|
||||||
if (resultCode == RESULT_OK) {
|
return;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Activity activity = (Activity) context;
|
||||||
|
|
||||||
private void showPermissionExplanationDialog() {
|
try {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
ClashUtil.startProxy(context); // 在主线程中调用
|
||||||
builder.setTitle("Permission Required")
|
} catch (IllegalStateException e) {
|
||||||
.setMessage("Storage Permission is required for the app to function. Please enable it in Settings.")
|
Toast.makeText(context, "Failed to start VPN: VPN Service illegal state", Toast.LENGTH_SHORT).show();
|
||||||
.setPositiveButton("Go to Settings", (dialog, which) -> {
|
} catch (Exception e) {
|
||||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
Toast.makeText(context, "Failed to start VPN: " + (e.getMessage() != null ? e.getMessage() : "Unknown error"), Toast.LENGTH_SHORT).show();
|
||||||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
|
||||||
intent.setData(uri);
|
|
||||||
startActivity(intent);
|
|
||||||
})
|
|
||||||
.setNegativeButton("Cancel", (dialog, which) -> finish())
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
super.onDestroy();
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
if (AutoJsUtil.scriptResultReceiver != null) {
|
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
|
||||||
unregisterReceiver(AutoJsUtil.scriptResultReceiver);
|
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) {
|
@Override
|
||||||
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||||
if (connectivityManager != null) {
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
Network network = connectivityManager.getActiveNetwork();
|
|
||||||
if (network != null) {
|
switch (requestCode) {
|
||||||
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
|
case ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE:
|
||||||
return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
handleStoragePermissionResult(resultCode);
|
||||||
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
|
break;
|
||||||
}
|
default:
|
||||||
}
|
break;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
package com.example.studyapp.device;
|
package com.example.studyapp.device;
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.ProxyInfo;
|
|
||||||
import android.util.Log;
|
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 com.example.studyapp.utils.ShellUtils;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
@ -18,7 +11,7 @@ import org.json.JSONObject;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public class ChangeDeviceInfo {
|
public class ChangeDeviceInfoUtil {
|
||||||
|
|
||||||
public static void changeDeviceInfo(String current_pkg_name,Context context) {
|
public static void changeDeviceInfo(String current_pkg_name,Context context) {
|
||||||
// 指定包名优先级高于全局
|
// 指定包名优先级高于全局
|
||||||
|
@ -81,12 +74,12 @@ public class ChangeDeviceInfo {
|
||||||
|
|
||||||
|
|
||||||
} catch (Throwable e) {
|
} 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);
|
throw new RuntimeException("Error occurred in changeDeviceInfo", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ShellUtils.hasRootAccess()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String> 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<String> getSystemDnsServers() {
|
|
||||||
List<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
|
@ -1,48 +1,60 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:layout_width="match_parent"
|
||||||
android:id="@+id/main"
|
android:layout_height="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:padding="16dp"
|
||||||
android:layout_height="match_parent"
|
android:background="@drawable/background"
|
||||||
android:orientation="vertical"
|
tools:context=".MainActivity">
|
||||||
android:gravity="center"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:background="@drawable/background"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<Button
|
<LinearLayout
|
||||||
android:id="@+id/run_script_button"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:orientation="vertical"
|
||||||
android:text="开始运行脚本"
|
android:gravity="center"
|
||||||
android:contentDescription="运行脚本按钮" />
|
android:padding="16dp">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/connectVpnButton"
|
android:id="@+id/run_script_button"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="开启vpn"
|
android:text="开始运行脚本"
|
||||||
android:contentDescription="开启 VPN 按钮" />
|
android:contentDescription="运行脚本按钮,用于启动自动化脚本" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/disconnectVpnButton"
|
android:id="@+id/connectVpnButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="断开vpn"
|
android:text="开启 VPN"
|
||||||
android:contentDescription="断开 VPN 按钮" />
|
android:contentDescription="开启 VPN 按钮,用于连接到指定 VPN 服务" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/modifyDeviceInfoButton"
|
android:id="@+id/disconnectVpnButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="修改设备信息"
|
android:text="断开 VPN"
|
||||||
android:contentDescription="修改设备信息按钮" />
|
android:contentDescription="断开 VPN 按钮,用于断开当前连接的 VPN" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/resetDeviceInfoButton"
|
android:id="@+id/switchVpnButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="重置设备信息"
|
android:text="切换 VPN"
|
||||||
android:contentDescription="重置设备信息按钮" />
|
android:contentDescription="切换 VPN 按钮,用于从一个 VPN 切换到另一个 VPN" />
|
||||||
</LinearLayout>
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/modifyDeviceInfoButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="修改设备信息"
|
||||||
|
android:contentDescription="修改设备信息按钮,用于编辑和更改设备信息" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/resetDeviceInfoButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="重置设备信息"
|
||||||
|
android:contentDescription="重置设备信息按钮,用于恢复到默认的设备信息" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">study.App</string>
|
<string name="app_name">study_app</string>
|
||||||
|
<string name="accessibility_service_description">This is an accessibility service.</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,5 +9,6 @@
|
||||||
<domain-config cleartextTrafficPermitted="true">
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
<domain includeSubdomains="true">47.236.153.142</domain>
|
<domain includeSubdomains="true">47.236.153.142</domain>
|
||||||
<domain includeSubdomains="true">8.211.204.20</domain>
|
<domain includeSubdomains="true">8.211.204.20</domain>
|
||||||
|
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||||
</domain-config>
|
</domain-config>
|
||||||
</network-security-config>
|
</network-security-config>
|
Loading…
Reference in New Issue