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:
parent
27a38d2a97
commit
c93475533d
|
@ -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>
|
|
@ -13,7 +13,6 @@
|
|||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
@ -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'
|
||||
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 |
|
@ -12,10 +12,11 @@ pluginManagement {
|
|||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue