Add VPN service, Shell utility, and server communication support

Introduced a `ProxyVpnService` to manage a VPN connection and a `ShellUtils` utility for executing commands. Enabled script result communication with the server using `CloudPhoneManageService` via Retrofit. Updated permissions, dependencies, and MainActivity to support these features.
This commit is contained in:
yjj38 2025-05-22 18:13:34 +08:00
parent 27a38d2a97
commit c93475533d
12 changed files with 447 additions and 4 deletions

View File

@ -5,6 +5,9 @@
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="Vpn">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -13,7 +13,6 @@
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@ -37,4 +37,13 @@ dependencies {
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Gson JSON /
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// RxJava
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
}

View File

@ -2,6 +2,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<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_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@ -1,27 +1,65 @@
package com.example.studyapp;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.VpnService;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Environment;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
import com.example.studyapp.request.ScriptResultRequest;
import com.example.studyapp.service.CloudPhoneManageService;
import com.example.studyapp.utils.ShellUtils;
import java.io.File;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
private BroadcastReceiver scriptResultReceiver;
private ActivityResultLauncher<Intent> vpnRequestLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
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) {
@ -30,6 +68,8 @@ public class MainActivity extends AppCompatActivity {
REQUEST_CODE_STORAGE_PERMISSION);
}
startProxyVpn(this);
// 查找按钮对象
Button runScriptButton = findViewById(R.id.run_script_button);
if (runScriptButton != null) {
@ -39,21 +79,46 @@ 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);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
permissionHandler();
Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
// 可选择终止操作或退出程序
finish(); // 假设应用需要此权限才能运行
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (scriptResultReceiver != null) {
unregisterReceiver(scriptResultReceiver);
}
}
private void runAutojsScript() {
// 定义脚本文件路径
File scriptFile = new File(getExternalFilesDir(null), "脚本/adsense.js");
File scriptFile = new File(Environment.getExternalStorageDirectory(), "脚本/chromium.js");
// 检查文件是否存在
if (!scriptFile.exists()) {
@ -75,6 +140,8 @@ public class MainActivity extends AppCompatActivity {
// 启动 Auto.js
try {
// 模拟通过广播监听脚本运行结果
registerScriptResultReceiver(); // 注册结果回调监听假设脚本通过广播返回结果
startActivity(intent);
Toast.makeText(this, "Running script: " + scriptFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
} catch (Exception e) {
@ -93,4 +160,82 @@ public class MainActivity extends AppCompatActivity {
return false;
}
}
private void registerScriptResultReceiver() {
// 创建广播接收器
scriptResultReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 获取脚本运行结果假设结果通过 "result" 键返回
String scriptResult = intent.getStringExtra("result");
if (scriptResult != null && !scriptResult.isEmpty()) {
// 处理结果并发送给服务端
sendResultToServer(scriptResult);
}
}
};
// 注册接收器假设 Auto.js 广播动作为 "org.autojs.SCRIPT_FINISHED"
IntentFilter filter = new IntentFilter("org.autojs.SCRIPT_FINISHED");
// 使用 ContextCompat.registerReceiver 注册并设置为 RECEIVER_EXPORTED
ContextCompat.registerReceiver(
this, // 当前上下文
scriptResultReceiver, // 自定义的 BroadcastReceiver
filter, // IntentFilter
ContextCompat.RECEIVER_EXPORTED // 设置为非导出广播接收器
);
}
private void sendResultToServer(String scriptResult) {
// 使用 Retrofit HttpURLConnection 实现服务端 API 调用
Toast.makeText(this, "Sending result to server: " + scriptResult, Toast.LENGTH_SHORT).show();
// 示例 Retrofit 设置服务端请求
// 创建 Retrofit 的实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://your-server-url.com/") // 替换为服务端 API 地址
.addConverterFactory(GsonConverterFactory.create())
.build();
// 定义 API 接口
CloudPhoneManageService api = retrofit.create(CloudPhoneManageService.class);
// 构建请求体并发送请求
Call<Void> call = api.sendScriptResult(new ScriptResultRequest(scriptResult)); // 假设参数是 com.example.studyapp.request.ScriptResultRequest 对象
call.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
if (response.isSuccessful()) {
Toast.makeText(MainActivity.this, "Result sent successfully", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "Failed to send result: " + response.code(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
Toast.makeText(MainActivity.this, "Error sending result: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
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");
}
}

View File

@ -0,0 +1,44 @@
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;
}
}

View File

@ -0,0 +1,18 @@
package com.example.studyapp.request;
// 这是发送到服务端的请求体JSON 格式
public class ScriptResultRequest {
private String result;
public ScriptResultRequest(String result) {
this.result = result;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}

View File

@ -0,0 +1,15 @@
package com.example.studyapp.service;
import com.example.studyapp.request.ScriptResultRequest;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;
public interface CloudPhoneManageService {
// 假设服务端接口接收 POST 请求
@POST("/api/script/result")
Call<Void> sendScriptResult(@Body ScriptResultRequest request);
}

View File

@ -0,0 +1,27 @@
package com.example.studyapp.utils;
public class DeviceUtils {
// 加载本地库
static {
try {
System.loadLibrary("native");
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
}
}
/** 设置设备ID */
public static native void setDeviceId(String deviceId);
/** 更新Boot ID */
public static native void updateBootId(String newBootId);
/** 获取系统信息 */
public static native long[] getSysInfo();
/** 修改CPU信息需系统权限 */
public static native boolean cpuInfoChange(String path);
/** 修改内存大小,需系统权限 */
public static native boolean changeMemSize(String path);
}

View File

@ -0,0 +1,172 @@
package com.example.studyapp.utils;
import java.io.BufferedReader;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class ShellUtils {
public static void exec(String cmd) {
try {
Log.e("ShellUtils", String.format("exec %s", cmd));
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
} catch (Exception e) {
}
}
public static int getPid(Process p) {
int pid = -1;
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
pid = f.getInt(p);
f.setAccessible(false);
} catch (Throwable e) {
pid = -1;
}
return pid;
}
public static boolean hasBin(String binName) {
if (!binName.matches("^[a-zA-Z0-9._-]+$")) {
throw new IllegalArgumentException("Invalid bin name");
}
String[] paths = System.getenv("PATH").split(":");
for (String path : paths) {
File file = new File(path + File.separator + binName);
try {
if (file.exists() && file.canExecute()) {
return true;
}
} catch (SecurityException e) {
Log.e("hasBin", "Security exception occurred: " + e.getMessage());
}
}
return false;
}
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;
}
Process process = null;
OutputStream os = null;
BufferedReader br = null;
try {
if (hasBin("su")) {
Log.e("ShellUtils", "Attempting to execute command: " + cmd);
process = Runtime.getRuntime().exec("su");
} else if (hasBin("xu")) {
process = Runtime.getRuntime().exec("xu");
} else if (hasBin("vu")) {
process = Runtime.getRuntime().exec("vu");
} else {
process = Runtime.getRuntime().exec("sh");
}
os = process.getOutputStream();
os.write((cmd + "\n").getBytes());
os.write(("exit\n").getBytes());
os.flush();
// Handle error stream on a separate thread
InputStream errorStream = process.getErrorStream();
new Thread(() -> {
try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream))) {
String errorLine;
while ((errorLine = errorReader.readLine()) != null) {
Log.e("ShellUtils", "Error: " + errorLine);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
process.waitFor();
br = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (process != null) {
process.destroy();
}
}
return null;
}
public static void execRootCmd(String cmd) {
if (!isCommandSafe(cmd)) {
Log.e("ShellUtils", "Unsafe command, aborting.");
return;
}
List<String> cmds = new ArrayList<>();
cmds.add(cmd);
execRootCmds(cmds);
}
private static boolean isCommandSafe(String cmd) {
return cmd.matches("^[a-zA-Z0-9._/:\\- ]+$");
}
public static void execRootCmds(List<String> cmds) {
Process process = null;
try {
if (hasBin("su")) {
process = Runtime.getRuntime().exec("su");
} else if (hasBin("xu")) {
process = Runtime.getRuntime().exec("xu");
} else if (hasBin("vu")) {
process = Runtime.getRuntime().exec("vu");
} else {
process = Runtime.getRuntime().exec("sh");
}
try (OutputStream os = process.getOutputStream()) {
for (String cmd : cmds) {
Log.e("ShellUtils", "Executing command");
os.write((cmd + "\n").getBytes());
}
os.write("exit\n".getBytes());
os.flush();
}
process.waitFor();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (process != null) {
process.destroy();
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -12,10 +12,11 @@ pluginManagement {
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
mavenLocal()
}
}