diff --git a/.idea/.name b/.idea/.name
index 821629e..a6b4d7b 100644
--- a/.idea/.name
+++ b/.idea/.name
@@ -1 +1 @@
-study.App
\ No newline at end of file
+V2rayUtil.java
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 31f028c..5acfdeb 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,10 +4,10 @@
-
+
-
+
diff --git a/app/src/main/java/com/example/studyapp/MainActivity.java b/app/src/main/java/com/example/studyapp/MainActivity.java
index 8c406d8..3946452 100644
--- a/app/src/main/java/com/example/studyapp/MainActivity.java
+++ b/app/src/main/java/com/example/studyapp/MainActivity.java
@@ -2,12 +2,12 @@ package com.example.studyapp;
import android.app.Activity;
import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.ServiceConnection;
import android.net.Uri;
import android.net.VpnService;
-import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
@@ -29,18 +29,12 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
+import com.example.studyapp.autoJS.AutoJsUtil;
import com.example.studyapp.proxy.CustomVpnService;
-import com.example.studyapp.request.ScriptResultRequest;
-import com.example.studyapp.service.CloudPhoneManageService;
+import com.example.studyapp.utils.ReflectionHelper;
-import java.io.File;
-import java.lang.ref.WeakReference;
-
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-import retrofit2.converter.gson.GsonConverterFactory;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_STORAGE_PERMISSION = 1;
@@ -48,12 +42,6 @@ public class MainActivity extends AppCompatActivity {
private static final int ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE = 1001;
- private static final int REQUEST_CODE_VPN = 2;
-
- private BroadcastReceiver scriptResultReceiver;
-
- private ActivityResultLauncher vpnRequestLauncher;
-
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -72,7 +60,7 @@ public class MainActivity extends AppCompatActivity {
}
} else {
// 针对 Android 11 及更高版本检查全文件管理权限
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ if (!Environment.isExternalStorageManager()) {
// 请求权限
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
@@ -80,13 +68,32 @@ public class MainActivity extends AppCompatActivity {
}
}
+ if (!isNetworkAvailable(this)) {
+ Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
+ finish();
+ }
// 初始化按钮
Button runScriptButton = findViewById(R.id.run_script_button);
if (runScriptButton != null) {
- runScriptButton.setOnClickListener(v -> runAutojsScript());
+ runScriptButton.setOnClickListener(v -> AutoJsUtil.runAutojsScript(this));
} else {
Toast.makeText(this, "Button not found", Toast.LENGTH_SHORT).show();
}
+
+ Button connectButton = findViewById(R.id.connectVpnButton);
+ if (connectButton != null) {
+ connectButton.setOnClickListener(v -> startProxyVpn(this));
+ } else {
+ Toast.makeText(this, "Connect button not found", Toast.LENGTH_SHORT).show();
+ }
+
+ 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();
+ }
+
}
private void startProxyVpn(Context context) {
@@ -130,55 +137,103 @@ public class MainActivity extends AppCompatActivity {
@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) {
- Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show();
-
- if (!isNetworkAvailable(this)) {
- Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
- return;
- }
-
- // 启动 VPN 服务
- startProxyVpn(this);
+ Toast.makeText(this, "Storage Permissions granted", Toast.LENGTH_SHORT).show();
} else {
+ // 提示权限被拒绝,同时允许用户重新授予权限
showPermissionExplanationDialog();
}
}
}
+
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == ALLOW_ALL_FILES_ACCESS_PERMISSION_CODE) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
- if (!isNetworkAvailable(this)) {
- Toast.makeText(this, "Network is not available", Toast.LENGTH_SHORT).show();
- return;
- }
- // 启动 VPN 服务
- startProxyVpn(this);
- } else {
- // 权限未授予,可提示用户
- Toast.makeText(this, "请授予所有文件管理权限", 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) {
+ 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 stopProxy(Context context) {
+ if (context == null) {
+ Log.e("stopProxy", "上下文为空,无法停止服务");
+ return;
}
- if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) {
- // Permission granted, now you can start your VpnService
+ boolean isServiceStopped = true;
+ try {
+ Object instance = ReflectionHelper.getInstance("com.example.studyapp.proxy.CustomVpnService", "instance");
+ if (instance != null) {
+ // 尝试获取 onDestroy 方法并调用
+ Method onDestroyMethod = instance.getClass().getMethod("onDestroy");
+ onDestroyMethod.invoke(instance);
+ Log.d("stopProxy", "服务已成功停止");
+ } else {
+ isServiceStopped = false;
+ Log.w("stopProxy", "实例为空,服务可能未启动");
+ }
+ } catch (NoSuchMethodException e) {
+ isServiceStopped = false;
+ Log.e("stopProxy", "服务未提供 onDestroy 方法: " + e.getMessage(), e);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ isServiceStopped = false;
+ Log.e("stopProxy", "无法调用 onDestroy 方法: " + 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());
+ }
+
+ private void handleVpnPermissionResult(int resultCode) {
+ if (resultCode == RESULT_OK) {
+
Intent intent = new Intent(this, CustomVpnService.class);
+
+ if (intent == null) {
+ Log.e("handleVpnPermissionResult", "Intent is null. Cannot start service.");
+ showToastOnUiThread(this, "Failed to start VPN service due to null intent.");
+ return;
+ }
+
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- this.startForegroundService(intent);
+ startForegroundService(intent);
} else {
- this.startService(intent);
+ startService(intent);
}
} catch (IllegalStateException e) {
- e.printStackTrace();
+ 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);
}
}
@@ -199,8 +254,8 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
super.onDestroy();
- if (scriptResultReceiver != null) {
- unregisterReceiver(scriptResultReceiver);
+ if (AutoJsUtil.scriptResultReceiver != null) {
+ unregisterReceiver(AutoJsUtil.scriptResultReceiver);
}
}
@@ -208,114 +263,12 @@ public class MainActivity extends AppCompatActivity {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
Network network = connectivityManager.getActiveNetwork();
- NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
- return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ 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;
}
-
-
- private void runAutojsScript() {
- // 定义脚本文件路径
- File scriptFile = new File(Environment.getExternalStorageDirectory(), "脚本/chromium.js");
-
- // 检查文件是否存在
- if (!scriptFile.exists()) {
- Toast.makeText(this, "Script file not found: " + scriptFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
- return;
- }
-
- // 检查 Auto.js 应用是否安装
- if (!isAppInstalled("org.autojs.autojs6")) {
- Toast.makeText(this, "Auto.js app not installed", Toast.LENGTH_SHORT).show();
- return;
- }
-
- // 准备启动 Auto.js 的 Intent
- Intent intent = new Intent();
- intent.setClassName("org.autojs.autojs6", "org.autojs.autojs.external.open.RunIntentActivity");
- intent.putExtra("path", scriptFile.getAbsolutePath()); // 传递脚本路径
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- // 启动 Auto.js
- try {
- // 模拟:通过广播监听脚本运行结果
- registerScriptResultReceiver(); // 注册结果回调监听(假设脚本通过广播返回结果)
- startActivity(intent);
- Toast.makeText(this, "Running script: " + scriptFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
- } catch (Exception e) {
- e.printStackTrace();
- Toast.makeText(this, "Failed to run script", Toast.LENGTH_SHORT).show();
- }
- }
-
- // 检查目标应用是否安装
- private boolean isAppInstalled(String packageName) {
- PackageManager packageManager = getPackageManager();
- try {
- packageManager.getPackageInfo(packageName, 0);
- return true;
- } catch (PackageManager.NameNotFoundException e) {
- 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 call = api.sendScriptResult(new ScriptResultRequest(scriptResult)); // 假设参数是 com.example.studyapp.request.ScriptResultRequest 对象
- call.enqueue(new Callback() {
- @Override
- public void onResponse(Call call, Response 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 call, Throwable t) {
- Toast.makeText(MainActivity.this, "Error sending result: " + t.getMessage(), Toast.LENGTH_SHORT).show();
- }
- });
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/studyapp/autoJS/AutoJsUtil.java b/app/src/main/java/com/example/studyapp/autoJS/AutoJsUtil.java
new file mode 100644
index 0000000..8b93305
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/autoJS/AutoJsUtil.java
@@ -0,0 +1,131 @@
+package com.example.studyapp.autoJS;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.widget.Toast;
+
+import androidx.core.content.ContextCompat;
+
+import com.example.studyapp.MainActivity;
+import com.example.studyapp.request.ScriptResultRequest;
+import com.example.studyapp.service.CloudPhoneManageService;
+
+import java.io.File;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+public class AutoJsUtil {
+
+ public static BroadcastReceiver scriptResultReceiver;
+
+ public static void runAutojsScript(Context context) {
+ // 定义脚本文件路径
+ File scriptFile = new File(Environment.getExternalStorageDirectory(), "脚本/chromium.js");
+
+ // 检查文件是否存在
+ if (!scriptFile.exists()) {
+ Toast.makeText(context, "Script file not found: " + scriptFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 检查 Auto.js 应用是否安装
+ if (!isAppInstalled("org.autojs.autojs6",context.getPackageManager())) {
+ Toast.makeText(context, "Auto.js app not installed", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 准备启动 Auto.js 的 Intent
+ Intent intent = new Intent();
+ intent.setClassName("org.autojs.autojs6", "org.autojs.autojs.external.open.RunIntentActivity");
+ intent.putExtra("path", scriptFile.getAbsolutePath()); // 传递脚本路径
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // 启动 Auto.js
+ try {
+ // 模拟:通过广播监听脚本运行结果
+ registerScriptResultReceiver(context); // 注册结果回调监听(假设脚本通过广播返回结果)
+ context.startActivity(intent);
+ Toast.makeText(context, "Running script: " + scriptFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(context, "Failed to run script", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // 检查目标应用是否安装
+ public static boolean isAppInstalled(String packageName,PackageManager packageManager) {
+ try {
+ packageManager.getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static void registerScriptResultReceiver(Context context) {
+ // 创建广播接收器
+ scriptResultReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // 获取脚本运行结果(假设结果通过 "result" 键返回)
+ String scriptResult = intent.getStringExtra("result");
+ if (scriptResult != null && !scriptResult.isEmpty()) {
+ // 处理结果并发送给服务端
+ sendResultToServer(scriptResult,context);
+ }
+ }
+ };
+
+ // 注册接收器(假设 Auto.js 广播动作为 "org.autojs.SCRIPT_FINISHED")
+ IntentFilter filter = new IntentFilter("org.autojs.SCRIPT_FINISHED");
+
+ // 使用 ContextCompat.registerReceiver 注册,并设置为 RECEIVER_EXPORTED
+ ContextCompat.registerReceiver(
+ context, // 当前上下文
+ scriptResultReceiver, // 自定义的 BroadcastReceiver
+ filter, // IntentFilter
+ ContextCompat.RECEIVER_EXPORTED // 设置为非导出广播接收器
+ );
+ }
+
+ public static void sendResultToServer(String scriptResult,Context context) {
+ // 使用 Retrofit 或 HttpURLConnection 实现服务端 API 调用
+ Toast.makeText(context, "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 call = api.sendScriptResult(new ScriptResultRequest(scriptResult)); // 假设参数是 com.example.studyapp.request.ScriptResultRequest 对象
+ call.enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Toast.makeText(context, "Result sent successfully", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(context, "Failed to send result: " + response.code(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Toast.makeText(context, "Error sending result: " + t.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java b/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java
new file mode 100644
index 0000000..78d39c7
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/device/ChangeDeviceInfo.java
@@ -0,0 +1,178 @@
+package com.example.studyapp.device;
+
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ProxyInfo;
+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 org.json.JSONObject;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class ChangeDeviceInfo {
+
+ public void changeDeviceInfo(String current_pkg_name,Context context) {
+ // 指定包名优先级高于全局
+ callVCloudSettings_put(current_pkg_name + "_android_id", "my123456",context);
+ callVCloudSettings_put(current_pkg_name + "_screen_brightness", "100",context);
+ callVCloudSettings_put(current_pkg_name + "_adb_enabled", "1",context);
+ callVCloudSettings_put(current_pkg_name + "_development_settings_enabled", "1",context);
+
+ callVCloudSettings_put("pm_list_features", "my_pm_list_features",context);
+ callVCloudSettings_put("pm_list_libraries", "my_pm_list_libraries",context);
+ callVCloudSettings_put("system.http.agent", "my_system.http.agent",context);
+ callVCloudSettings_put("webkit.http.agent", "my_webkit.http.agent",context);
+
+ callVCloudSettings_put("global_android_id", "123456",context);
+
+ callVCloudSettings_put("anticheck_pkgs", current_pkg_name,context);
+
+ try {
+
+ JSONObject pkg_info_json = new JSONObject();
+ pkg_info_json.put("versionName", "1.0.0");
+ pkg_info_json.put("versionCode", 100);
+ pkg_info_json.put("firstInstallTime", 1);
+ pkg_info_json.put("lastUpdateTime", 1);
+ callVCloudSettings_put("com.fk.tools_pkgInfo", pkg_info_json.toString(),context);
+
+ JSONObject tmp_json = new JSONObject();
+ tmp_json.put("widthPixels", 1080);
+ tmp_json.put("heightPixels", 1920);
+ tmp_json.put("densityDpi", 440);
+ tmp_json.put("xdpi", 160);
+ tmp_json.put("ydpi", 160);
+ tmp_json.put("density", 3.0);
+ tmp_json.put("scaledDensity", 3.0);
+ callVCloudSettings_put("screen.getDisplayMetrics", tmp_json.toString(),context);
+ callVCloudSettings_put("screen.getMetrics", tmp_json.toString(),context);
+ callVCloudSettings_put("screen.getRealMetrics", tmp_json.toString(),context);
+ callVCloudSettings_put(current_pkg_name + "_screen.getDisplayMetrics.stack", ".getDeviceInfo",context);
+ String stackInfo = Thread.currentThread().getStackTrace()[2].toString();
+ callVCloudSettings_put(current_pkg_name + "_screen.getMetrics.stack", stackInfo, context);
+ callVCloudSettings_put(current_pkg_name + "_screen.getRealMetrics.stack", ".getDeviceInfo",context);
+
+
+ tmp_json = new JSONObject();
+ tmp_json.put("width", 1080);
+ tmp_json.put("height", 1820);
+ callVCloudSettings_put("screen.getRealSize", tmp_json.toString(),context);
+ callVCloudSettings_put(current_pkg_name + "_screen.getRealSize.stack", ".getDeviceInfo",context);
+
+
+ tmp_json = new JSONObject();
+ tmp_json.put("left", 0);
+ tmp_json.put("top", 0);
+ tmp_json.put("right", 1080);
+ tmp_json.put("bottom", 1920);
+ callVCloudSettings_put("screen.getCurrentBounds", tmp_json.toString(),context);
+ callVCloudSettings_put("screen.getMaximumBounds", tmp_json.toString(),context);
+ callVCloudSettings_put(current_pkg_name + "_screen.getCurrentBounds.stack", ".getDeviceInfo",context);
+ callVCloudSettings_put(current_pkg_name + "_screen.getMaximumBounds.stack", ".getDeviceInfo",context);
+
+
+ } catch (Throwable e) {
+ Log.e("ChangeDeviceInfo", "Error occurred while changing device info", e);
+ throw new RuntimeException("Error occurred in changeDeviceInfo", e);
+ }
+
+ if (!ShellUtils.hasRootAccess()) {
+ Log.e("ChangeDeviceInfo", "Root access is required to execute system property changes");
+ return;
+ }
+
+ // 设置机型, 直接设置属性
+ ShellUtils.execRootCmd("setprop ro.product.brand google");
+ ShellUtils.execRootCmd("setprop ro.product.model raven");
+ ShellUtils.execRootCmd("setprop ro.product.manufacturer google");
+ ShellUtils.execRootCmd("setprop ro.product.device raven");
+ ShellUtils.execRootCmd("setprop ro.product.name raven");
+ ShellUtils.execRootCmd("setprop ro.build.version.incremental 9325679");
+ ShellUtils.execRootCmd("setprop ro.build.fingerprint \"google/raven/raven:13/TQ1A.230105.002/9325679:user/release-keys\"");
+ ShellUtils.execRootCmd("setprop ro.board.platform acr980m");
+
+ Native.setBootId("400079ef55a4475558eb60a0544a43d5");
+
+ // 修改drm id
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.drm.id 400079ef55a4475558eb60a0544a43d5171258f13fdd48c10026e2847a6fc7a5");
+
+ // 电量模拟需要大于1000
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.battery.capacity 5000");
+
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor my_gl_vendor");
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer my_gl_renderer");
+ // 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version \"OpenGL ES 3.2\"");
+
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor my_egl_vendor");
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_version my_egl_version");
+
+ }
+
+ private void callVCloudSettings_put(String key, String value, Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context cannot be null");
+ }
+
+ try {
+ Class> clazz = Class.forName("android.provider.VCloudSettings$Global");
+ Method putStringMethod = clazz.getDeclaredMethod("putString", ContentResolver.class, String.class, String.class);
+ putStringMethod.setAccessible(true);
+ putStringMethod.invoke(null, context.getContentResolver(), key, value);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Class VCloudSettings$Global not found", e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Method putString not found", e);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Failed to invoke putString via reflection", e);
+ }
+ }
+
+ private void resetChangedDeviceInfo(String current_pkg_name,Context context) {
+ try {
+ Native.setBootId("00000000000000000000000000000000");
+ } catch (Exception e) {
+ Log.e("resetChangedDeviceInfo", "Failed to set boot ID", e);
+ }
+
+ if (!ShellUtils.hasRootAccess()) {
+ Log.e("resetChangedDeviceInfo", "Root privileges are required.");
+ return;
+ }
+ ShellUtils.execRootCmd("cmd settings2 delete global global_android_id");
+ ShellUtils.execRootCmd("cmd settings2 delete global pm_list_features");
+ ShellUtils.execRootCmd("cmd settings2 delete global pm_list_libraries");
+ ShellUtils.execRootCmd("cmd settings2 delete global anticheck_pkgs");
+ ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_android_id");
+ ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_adb_enabled");
+ ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_development_settings_enabled");
+
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.drm.id \"\"");
+
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor \"\"");
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer \"\"");
+ // 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version \"\"");
+
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor \"\"");
+ ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_version \"\"");
+
+ ShellUtils.execRootCmd("setprop ro.product.brand Vortex");
+ ShellUtils.execRootCmd("setprop ro.product.model HD65_Select");
+ ShellUtils.execRootCmd("setprop ro.product.manufacturer Vortex");
+ ShellUtils.execRootCmd("setprop ro.product.device HD65_Select");
+ ShellUtils.execRootCmd("setprop ro.product.name HD65_Select");
+ ShellUtils.execRootCmd("setprop ro.build.version.incremental 20240306");
+ ShellUtils.execRootCmd("setprop ro.build.fingerprint \"Vortex/HD65_Select/HD65_Select:13/TP1A.220624.014/20240306:user/release-keys\"");
+ ShellUtils.execRootCmd("setprop ro.board.platform sm8150p");
+ }
+}
diff --git a/app/src/main/java/com/example/studyapp/device/Native.java b/app/src/main/java/com/example/studyapp/device/Native.java
new file mode 100644
index 0000000..3d4f296
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/device/Native.java
@@ -0,0 +1,28 @@
+package com.example.studyapp.device;
+
+public class Native {
+ static {
+ try {
+ System.loadLibrary("native");
+ } catch (UnsatisfiedLinkError e) {
+ e.printStackTrace();
+ // 添加日志或通知以便诊断问题
+ }
+ }
+
+ public native static long[] getSysInfo();
+
+ public native static void getPower();
+
+ public native static void enableBypassAntiDebug(int uid);
+
+ public native static void setBootId(String bootIdHexStr);
+
+ public native static void deviceInfoShow();
+ public native static void deviceInfoChange();
+
+ public native static void deviceInfoReset();
+
+ public native static boolean cpuInfoChange(String path);
+ public native static boolean changeMemSize(String path);
+}
diff --git a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java b/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java
index babb1c7..b106b17 100644
--- a/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java
+++ b/app/src/main/java/com/example/studyapp/proxy/CustomVpnService.java
@@ -1,7 +1,10 @@
package com.example.studyapp.proxy;
+import static com.example.studyapp.utils.V2rayUtil.isV2rayRunning;
+
import android.content.Intent;
import android.net.VpnService;
+import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
@@ -26,10 +29,12 @@ public class CustomVpnService extends VpnService {
private static final String TUN_ADDRESS = ConfigLoader.getTunnelAddress(); // TUN 的 IP 地址
private static final int PREFIX_LENGTH = 28; // 子网掩码
+ private Thread vpnTrafficThread; // 保存线程引用
private ParcelFileDescriptor vpnInterface; // TUN 接口描述符
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
+ isVpnActive = true; // 服务启动时激活
try {
// 检查 V2ray 是否已启动,避免重复进程
if (!isV2rayRunning()) {
@@ -46,34 +51,6 @@ public class CustomVpnService extends VpnService {
return START_STICKY;
}
- private boolean isV2rayRunning() {
- try {
- // 执行系统命令,获取当前所有正在运行的进程
- Process process = Runtime.getRuntime().exec("ps");
-
- // 读取进程的输出
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
- String line;
- while ((line = reader.readLine()) != null) {
- // 检查是否有包含 "v2ray" 的进程
- if (line.contains("v2ray")) {
- Log.i("CustomVpnService", "V2Ray process found: " + line);
- return true;
- }
- }
- }
-
- // 检查完成,没有找到 "v2ray" 相关的进程
- Log.i("CustomVpnService", "No V2Ray process is running.");
- return false;
-
- } catch (IOException e) {
- // 捕获异常并记录日志
- Log.e("CustomVpnService", "Error checking V2Ray process: " + e.getMessage(), e);
- return false;
- }
- }
-
private void startVpn() {
try {
// 配置虚拟网卡
@@ -161,77 +138,102 @@ public class CustomVpnService extends VpnService {
return dnsServers;
}
+ private static CustomVpnService instance;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ instance = this; // 在创建时将实例存储到静态字段
+ }
+
+ public static CustomVpnService getInstance() {
+ return instance;
+ }
+
@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();
- vpnInterface = null;
- Log.d("CustomVpnService", "VPN interface closed.");
} catch (IOException e) {
- Log.e("CustomVpnService", "Error closing VPN interface", e);
+ Log.e("CustomVpnService", "Error closing VPN interface: " + e.getMessage(), e);
}
+ vpnInterface = null; // 避免资源泄露
}
+
+ // 停止 V2Ray 服务
+ V2rayUtil.stopV2Ray();
+ Log.i("CustomVpnService", "VPN 服务已销毁");
}
+ private volatile boolean isVpnActive = false; // 标志位控制数据包处理逻辑
+
private void handleVpnTraffic(ParcelFileDescriptor vpnInterface) {
- if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
- throw new IllegalArgumentException("ParcelFileDescriptor is invalid!");
- }
+ // 启动处理流量的线程
+ vpnTrafficThread = new Thread(() -> {
+ if (vpnInterface == null || !vpnInterface.getFileDescriptor().valid()) {
+ Log.e("CustomVpnService", "ParcelFileDescriptor is invalid!");
+ return;
+ }
- byte[] packetData = new byte[32767];
- final int maxRetry = 5; // 定义最大重试次数
- int retryCount = 0;
+ byte[] packetData = new byte[32767]; // 数据包缓冲区
- FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
- FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor());
- // 将流放在 try-with-resources 之外,避免循环中被关闭
+ try (FileInputStream inStream = new FileInputStream(vpnInterface.getFileDescriptor());
+ FileOutputStream outStream = new FileOutputStream(vpnInterface.getFileDescriptor())) {
- // 循环处理 VPN 数据包
- while (!Thread.currentThread().isInterrupted()) {
- try {
- int length = inStream.read(packetData);
- if (length == -1) {
- // 读取结束
- break;
- }
+ 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);
+ if (length > 0) {
+ boolean handled = processPacket(packetData, length);
+ if (!handled) {
+ outStream.write(packetData, 0, length); // 未处理的包写回
+ }
}
}
- retryCount = 0; // 成功一次后重置重试次数
} catch (IOException e) {
- retryCount++;
- Log.e("CustomVpnService", "Error reading packet. Retry attempt " + retryCount, e);
- if (retryCount >= maxRetry) {
- Log.e("CustomVpnService", "Max retry reached. Exiting loop.");
- break;
- }
- // 可添加短暂延迟来避免频繁重试
+ Log.e("CustomVpnService", "Error handling VPN traffic", e);
+ } finally {
try {
- Thread.sleep(100);
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- Log.e("CustomVpnService", "Thread interrupted during sleep", ie);
+ vpnInterface.close();
+ } catch (IOException e) {
+ Log.e("CustomVpnService", "Failed to close vpnInterface", e);
}
}
- }
-
- // 最终关闭 ParcelFileDescriptor
- 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;
@@ -283,4 +285,5 @@ public class CustomVpnService extends VpnService {
// 检查是否是 UDP 的 DNS 端口 (53)
return sourcePort == 53 || destPort == 53;
- }}
+ }
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/ClassUtils.java b/app/src/main/java/com/example/studyapp/utils/ClassUtils.java
new file mode 100644
index 0000000..4df67a3
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/utils/ClassUtils.java
@@ -0,0 +1,1027 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.studyapp.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Operates on classes without using reflection.
+ *
+ * This class handles invalid {@code null} inputs as best it can.
+ * Each method documents its behaviour in more detail.
+ *
+ * The notion of a {@code canonical name} includes the human
+ * readable name for the type, for example {@code int[]}. The
+ * non-canonical method variants work with the JVM names, such as
+ * {@code [I}.
+ *
+ * @since 2.0
+ * @version $Id: ClassUtils.java 1199894 2011-11-09 17:53:59Z ggregory $
+ */
+public class ClassUtils {
+
+ /**
+ * The package separator character: '.' == {@value}
.
+ */
+ public static final char PACKAGE_SEPARATOR_CHAR = '.';
+
+ /**
+ * The package separator String: {@code "."}.
+ */
+ public static final String PACKAGE_SEPARATOR = String.valueOf(PACKAGE_SEPARATOR_CHAR);
+
+ /**
+ * The inner class separator character: '$' == {@value}
.
+ */
+ public static final char INNER_CLASS_SEPARATOR_CHAR = '$';
+
+ /**
+ * The inner class separator String: {@code "$"}.
+ */
+ public static final String INNER_CLASS_SEPARATOR = String.valueOf(INNER_CLASS_SEPARATOR_CHAR);
+
+ /**
+ * Maps primitive {@code Class}es to their corresponding wrapper {@code Class}.
+ */
+ private static final Map, Class>> primitiveWrapperMap = new HashMap, Class>>();
+ static {
+ primitiveWrapperMap.put(Boolean.TYPE, Boolean.class);
+ primitiveWrapperMap.put(Byte.TYPE, Byte.class);
+ primitiveWrapperMap.put(Character.TYPE, Character.class);
+ primitiveWrapperMap.put(Short.TYPE, Short.class);
+ primitiveWrapperMap.put(Integer.TYPE, Integer.class);
+ primitiveWrapperMap.put(Long.TYPE, Long.class);
+ primitiveWrapperMap.put(Double.TYPE, Double.class);
+ primitiveWrapperMap.put(Float.TYPE, Float.class);
+ primitiveWrapperMap.put(Void.TYPE, Void.TYPE);
+ }
+
+ /**
+ * Maps wrapper {@code Class}es to their corresponding primitive types.
+ */
+ private static final Map, Class>> wrapperPrimitiveMap = new HashMap, Class>>();
+ static {
+ for (Class> primitiveClass : primitiveWrapperMap.keySet()) {
+ Class> wrapperClass = primitiveWrapperMap.get(primitiveClass);
+ if (!primitiveClass.equals(wrapperClass)) {
+ wrapperPrimitiveMap.put(wrapperClass, primitiveClass);
+ }
+ }
+ }
+
+ /**
+ * Maps a primitive class name to its corresponding abbreviation used in array class names.
+ */
+ private static final Map abbreviationMap = new HashMap();
+
+ /**
+ * Maps an abbreviation used in array class names to corresponding primitive class name.
+ */
+ private static final Map reverseAbbreviationMap = new HashMap();
+
+ /**
+ * Add primitive type abbreviation to maps of abbreviations.
+ *
+ * @param primitive Canonical name of primitive type
+ * @param abbreviation Corresponding abbreviation of primitive type
+ */
+ private static void addAbbreviation(String primitive, String abbreviation) {
+ abbreviationMap.put(primitive, abbreviation);
+ reverseAbbreviationMap.put(abbreviation, primitive);
+ }
+
+ /**
+ * Feed abbreviation maps
+ */
+ static {
+ addAbbreviation("int", "I");
+ addAbbreviation("boolean", "Z");
+ addAbbreviation("float", "F");
+ addAbbreviation("long", "J");
+ addAbbreviation("short", "S");
+ addAbbreviation("byte", "B");
+ addAbbreviation("double", "D");
+ addAbbreviation("char", "C");
+ }
+
+ /**
+ * ClassUtils instances should NOT be constructed in standard programming.
+ * Instead, the class should be used as
+ * {@code ClassUtils.getShortClassName(cls)}.
+ *
+ * This constructor is public to permit tools that require a JavaBean
+ * instance to operate.
+ */
+ public ClassUtils() {
+ super();
+ }
+
+ // Short class name
+ // ----------------------------------------------------------------------
+ /**
+ * Gets the class name minus the package name for an {@code Object}.
+ *
+ * @param object the class to get the short name for, may be null
+ * @param valueIfNull the value to return if null
+ * @return the class name of the object without the package name, or the null value
+ */
+ public static String getShortClassName(Object object, String valueIfNull) {
+ if (object == null) {
+ return valueIfNull;
+ }
+ return getShortClassName(object.getClass());
+ }
+
+ /**
+ * Gets the class name minus the package name from a {@code Class}.
+ *
+ * Consider using the Java 5 API {@link Class#getSimpleName()} instead.
+ * The one known difference is that this code will return {@code "Map.Entry"} while
+ * the {@code java.lang.Class} variant will simply return {@code "Entry"}.
+ *
+ * @param cls the class to get the short name for.
+ * @return the class name without the package name or an empty string
+ */
+ public static String getShortClassName(Class> cls) {
+ if (cls == null) {
+ return "";
+ }
+ return getShortClassName(cls.getName());
+ }
+
+ /**
+ * Gets the class name minus the package name from a String.
+ *
+ * The string passed in is assumed to be a class name - it is not checked.
+
+ * Note that this method differs from Class.getSimpleName() in that this will
+ * return {@code "Map.Entry"} whilst the {@code java.lang.Class} variant will simply
+ * return {@code "Entry"}.
+ *
+ * @param className the className to get the short name for
+ * @return the class name of the class without the package name or an empty string
+ */
+ public static String getShortClassName(String className) {
+ if (className == null) {
+ return "";
+ }
+ if (className.length() == 0) {
+ return "";
+ }
+
+ StringBuilder arrayPrefix = new StringBuilder();
+
+ // Handle array encoding
+ if (className.startsWith("[")) {
+ while (className.charAt(0) == '[') {
+ className = className.substring(1);
+ arrayPrefix.append("[]");
+ }
+ // Strip Object type encoding
+ if (className.charAt(0) == 'L' && className.charAt(className.length() - 1) == ';') {
+ className = className.substring(1, className.length() - 1);
+ }
+ }
+
+ if (reverseAbbreviationMap.containsKey(className)) {
+ className = reverseAbbreviationMap.get(className);
+ }
+
+ int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
+ int innerIdx = className.indexOf(
+ INNER_CLASS_SEPARATOR_CHAR, lastDotIdx == -1 ? 0 : lastDotIdx + 1);
+ String out = className.substring(lastDotIdx + 1);
+ if (innerIdx != -1) {
+ out = out.replace(INNER_CLASS_SEPARATOR_CHAR, PACKAGE_SEPARATOR_CHAR);
+ }
+ return out + arrayPrefix;
+ }
+
+ /**
+ * Null-safe version of aClass.getSimpleName()
+ *
+ * @param cls the class for which to get the simple name.
+ * @return the simple class name.
+ * @since 3.0
+ * @see Class#getSimpleName()
+ */
+ public static String getSimpleName(Class> cls) {
+ if (cls == null) {
+ return "";
+ }
+ return cls.getSimpleName();
+ }
+
+ /**
+ * Null-safe version of aClass.getSimpleName()
+ *
+ * @param object the object for which to get the simple class name.
+ * @param valueIfNull the value to return if object
is null
+ * @return the simple class name.
+ * @since 3.0
+ * @see Class#getSimpleName()
+ */
+ public static String getSimpleName(Object object, String valueIfNull) {
+ if (object == null) {
+ return valueIfNull;
+ }
+ return getSimpleName(object.getClass());
+ }
+
+ // Package name
+ // ----------------------------------------------------------------------
+ /**
+ * Gets the package name of an {@code Object}.
+ *
+ * @param object the class to get the package name for, may be null
+ * @param valueIfNull the value to return if null
+ * @return the package name of the object, or the null value
+ */
+ public static String getPackageName(Object object, String valueIfNull) {
+ if (object == null) {
+ return valueIfNull;
+ }
+ return getPackageName(object.getClass());
+ }
+
+ /**
+ * Gets the package name of a {@code Class}.
+ *
+ * @param cls the class to get the package name for, may be {@code null}.
+ * @return the package name or an empty string
+ */
+ public static String getPackageName(Class> cls) {
+ if (cls == null) {
+ return "";
+ }
+ return getPackageName(cls.getName());
+ }
+
+ /**
+ * Gets the package name from a {@code String}.
+ *
+ * The string passed in is assumed to be a class name - it is not checked.
+ * If the class is unpackaged, return an empty string.
+ *
+ * @param className the className to get the package name for, may be {@code null}
+ * @return the package name or an empty string
+ */
+ public static String getPackageName(String className) {
+ if (className == null || className.length() == 0) {
+ return "";
+ }
+
+ // Strip array encoding
+ while (className.charAt(0) == '[') {
+ className = className.substring(1);
+ }
+ // Strip Object type encoding
+ if (className.charAt(0) == 'L' && className.charAt(className.length() - 1) == ';') {
+ className = className.substring(1);
+ }
+
+ int i = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
+ if (i == -1) {
+ return "";
+ }
+ return className.substring(0, i);
+ }
+
+ // Superclasses/Superinterfaces
+ // ----------------------------------------------------------------------
+ /**
+ * Gets a {@code List} of superclasses for the given class.
+ *
+ * @param cls the class to look up, may be {@code null}
+ * @return the {@code List} of superclasses in order going up from this one
+ * {@code null} if null input
+ */
+ public static List> getAllSuperclasses(Class> cls) {
+ if (cls == null) {
+ return null;
+ }
+ List> classes = new ArrayList>();
+ Class> superclass = cls.getSuperclass();
+ while (superclass != null) {
+ classes.add(superclass);
+ superclass = superclass.getSuperclass();
+ }
+ return classes;
+ }
+
+ /**
+ * Gets a {@code List} of all interfaces implemented by the given
+ * class and its superclasses.
+ *
+ * The order is determined by looking through each interface in turn as
+ * declared in the source file and following its hierarchy up. Then each
+ * superclass is considered in the same way. Later duplicates are ignored,
+ * so the order is maintained.
+ *
+ * @param cls the class to look up, may be {@code null}
+ * @return the {@code List} of interfaces in order,
+ * {@code null} if null input
+ */
+ public static List> getAllInterfaces(Class> cls) {
+ if (cls == null) {
+ return null;
+ }
+
+ LinkedHashSet> interfacesFound = new LinkedHashSet>();
+ getAllInterfaces(cls, interfacesFound);
+
+ return new ArrayList>(interfacesFound);
+ }
+
+ /**
+ * Get the interfaces for the specified class.
+ *
+ * @param cls the class to look up, may be {@code null}
+ * @param interfacesFound the {@code Set} of interfaces for the class
+ */
+ private static void getAllInterfaces(Class> cls, HashSet> interfacesFound) {
+ while (cls != null) {
+ Class>[] interfaces = cls.getInterfaces();
+
+ for (Class> i : interfaces) {
+ if (interfacesFound.add(i)) {
+ getAllInterfaces(i, interfacesFound);
+ }
+ }
+
+ cls = cls.getSuperclass();
+ }
+ }
+
+ // Convert list
+ // ----------------------------------------------------------------------
+ /**
+ * Given a {@code List} of class names, this method converts them into classes.
+ *
+ * A new {@code List} is returned. If the class name cannot be found, {@code null}
+ * is stored in the {@code List}. If the class name in the {@code List} is
+ * {@code null}, {@code null} is stored in the output {@code List}.
+ *
+ * @param classNames the classNames to change
+ * @return a {@code List} of Class objects corresponding to the class names,
+ * {@code null} if null input
+ * @throws ClassCastException if classNames contains a non String entry
+ */
+ public static List> convertClassNamesToClasses(List classNames) {
+ if (classNames == null) {
+ return null;
+ }
+ List> classes = new ArrayList>(classNames.size());
+ for (String className : classNames) {
+ try {
+ classes.add(Class.forName(className));
+ } catch (Exception ex) {
+ classes.add(null);
+ }
+ }
+ return classes;
+ }
+
+ /**
+ * Given a {@code List} of {@code Class} objects, this method converts
+ * them into class names.
+ *
+ * A new {@code List} is returned. {@code null} objects will be copied into
+ * the returned list as {@code null}.
+ *
+ * @param classes the classes to change
+ * @return a {@code List} of class names corresponding to the Class objects,
+ * {@code null} if null input
+ * @throws ClassCastException if {@code classes} contains a non-{@code Class} entry
+ */
+ public static List convertClassesToClassNames(List> classes) {
+ if (classes == null) {
+ return null;
+ }
+ List classNames = new ArrayList(classes.size());
+ for (Class> cls : classes) {
+ if (cls == null) {
+ classNames.add(null);
+ } else {
+ classNames.add(cls.getName());
+ }
+ }
+ return classNames;
+ }
+
+ /**
+ * Checks if an array of Classes can be assigned to another array of Classes.
+ *
+ * This method calls for each
+ * Class pair in the input arrays. It can be used to check if a set of arguments
+ * (the first parameter) are suitably compatible with a set of method parameter types
+ * (the second parameter).
+ *
+ * Unlike the {@link Class#isAssignableFrom(Class)} method, this
+ * method takes into account widenings of primitive classes and
+ * {@code null}s.
+ *
+ * Primitive widenings allow an int to be assigned to a {@code long},
+ * {@code float} or {@code double}. This method returns the correct
+ * result for these cases.
+ *
+ * {@code Null} may be assigned to any reference type. This method will
+ * return {@code true} if {@code null} is passed in and the toClass is
+ * non-primitive.
+ *
+ * Specifically, this method tests whether the type represented by the
+ * specified {@code Class} parameter can be converted to the type
+ * represented by this {@code Class} object via an identity conversion
+ * widening primitive or widening reference conversion. See
+ * The Java Language Specification,
+ * sections 5.1.1, 5.1.2 and 5.1.4 for details.
+ *
+ * @param classArray the array of Classes to check, may be {@code null}
+ * @param toClassArray the array of Classes to try to assign into, may be {@code null}
+ * @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
+ * @return {@code true} if assignment possible
+ */
+ public static boolean isAssignable(Class>[] classArray, Class>[] toClassArray, boolean autoboxing) {
+ if ((classArray == null ? 0 : classArray.length) != (toClassArray == null ? 0 : toClassArray.length)) {
+ return false;
+ }
+ if (classArray == null) {
+ classArray = new Class>[0];
+ }
+ if (toClassArray == null) {
+ toClassArray = new Class>[0];
+ }
+ for (int i = 0; i < classArray.length; i++) {
+ if (!isAssignable(classArray[i], toClassArray[i], autoboxing)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether the given {@code type} is a primitive or primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character},
+ * {@link Short}, {@link Integer}, {@link Long}, {@link Double}, {@link Float}).
+ *
+ * @param type
+ * The class to query or null.
+ * @return true if the given {@code type} is a primitive or primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character},
+ * {@link Short}, {@link Integer}, {@link Long}, {@link Double}, {@link Float}).
+ * @since 3.1
+ */
+ public static boolean isPrimitiveOrWrapper(Class> type) {
+ if (type == null) {
+ return false;
+ }
+ return type.isPrimitive() || isPrimitiveWrapper(type);
+ }
+
+ /**
+ * Returns whether the given {@code type} is a primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character}, {@link Short},
+ * {@link Integer}, {@link Long}, {@link Double}, {@link Float}).
+ *
+ * @param type
+ * The class to query or null.
+ * @return true if the given {@code type} is a primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character}, {@link Short},
+ * {@link Integer}, {@link Long}, {@link Double}, {@link Float}).
+ * @since 3.1
+ */
+ public static boolean isPrimitiveWrapper(Class> type) {
+ return wrapperPrimitiveMap.containsKey(type);
+ }
+
+ /**
+ * Checks if one {@code Class} can be assigned to a variable of
+ * another {@code Class}.
+ *
+ * Unlike the {@link Class#isAssignableFrom(Class)} method,
+ * this method takes into account widenings of primitive classes and
+ * {@code null}s.
+ *
+ * Primitive widenings allow an int to be assigned to a long, float or
+ * double. This method returns the correct result for these cases.
+ *
+ * {@code Null} may be assigned to any reference type. This method
+ * will return {@code true} if {@code null} is passed in and the
+ * toClass is non-primitive.
+ *
+ * Specifically, this method tests whether the type represented by the
+ * specified {@code Class} parameter can be converted to the type
+ * represented by this {@code Class} object via an identity conversion
+ * widening primitive or widening reference conversion. See
+ * The Java Language Specification,
+ * sections 5.1.1, 5.1.2 and 5.1.4 for details.
+ *
+ * @param cls the Class to check, may be null
+ * @param toClass the Class to try to assign into, returns false if null
+ * @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
+ * @return {@code true} if assignment possible
+ */
+ public static boolean isAssignable(Class> cls, Class> toClass, boolean autoboxing) {
+ if (toClass == null) {
+ return false;
+ }
+ // have to check for null, as isAssignableFrom doesn't
+ if (cls == null) {
+ return !toClass.isPrimitive();
+ }
+ //autoboxing:
+ if (autoboxing) {
+ if (cls.isPrimitive() && !toClass.isPrimitive()) {
+ cls = primitiveToWrapper(cls);
+ if (cls == null) {
+ return false;
+ }
+ }
+ if (toClass.isPrimitive() && !cls.isPrimitive()) {
+ cls = wrapperToPrimitive(cls);
+ if (cls == null) {
+ return false;
+ }
+ }
+ }
+ if (cls.equals(toClass)) {
+ return true;
+ }
+ if (cls.isPrimitive()) {
+ if (toClass.isPrimitive() == false) {
+ return false;
+ }
+ if (Integer.TYPE.equals(cls)) {
+ return Long.TYPE.equals(toClass)
+ || Float.TYPE.equals(toClass)
+ || Double.TYPE.equals(toClass);
+ }
+ if (Long.TYPE.equals(cls)) {
+ return Float.TYPE.equals(toClass)
+ || Double.TYPE.equals(toClass);
+ }
+ if (Boolean.TYPE.equals(cls)) {
+ return false;
+ }
+ if (Double.TYPE.equals(cls)) {
+ return false;
+ }
+ if (Float.TYPE.equals(cls)) {
+ return Double.TYPE.equals(toClass);
+ }
+ if (Character.TYPE.equals(cls)) {
+ return Integer.TYPE.equals(toClass)
+ || Long.TYPE.equals(toClass)
+ || Float.TYPE.equals(toClass)
+ || Double.TYPE.equals(toClass);
+ }
+ if (Short.TYPE.equals(cls)) {
+ return Integer.TYPE.equals(toClass)
+ || Long.TYPE.equals(toClass)
+ || Float.TYPE.equals(toClass)
+ || Double.TYPE.equals(toClass);
+ }
+ if (Byte.TYPE.equals(cls)) {
+ return Short.TYPE.equals(toClass)
+ || Integer.TYPE.equals(toClass)
+ || Long.TYPE.equals(toClass)
+ || Float.TYPE.equals(toClass)
+ || Double.TYPE.equals(toClass);
+ }
+ // should never get here
+ return false;
+ }
+ return toClass.isAssignableFrom(cls);
+ }
+
+ /**
+ * Converts the specified primitive Class object to its corresponding
+ * wrapper Class object.
+ *
+ * NOTE: From v2.2, this method handles {@code Void.TYPE},
+ * returning {@code Void.TYPE}.
+ *
+ * @param cls the class to convert, may be null
+ * @return the wrapper class for {@code cls} or {@code cls} if
+ * {@code cls} is not a primitive. {@code null} if null input.
+ * @since 2.1
+ */
+ public static Class> primitiveToWrapper(Class> cls) {
+ Class> convertedClass = cls;
+ if (cls != null && cls.isPrimitive()) {
+ convertedClass = primitiveWrapperMap.get(cls);
+ }
+ return convertedClass;
+ }
+
+ /**
+ * Converts the specified array of primitive Class objects to an array of
+ * its corresponding wrapper Class objects.
+ *
+ * @param classes the class array to convert, may be null or empty
+ * @return an array which contains for each given class, the wrapper class or
+ * the original class if class is not a primitive. {@code null} if null input.
+ * Empty array if an empty array passed in.
+ * @since 2.1
+ */
+ public static Class>[] primitivesToWrappers(Class>... classes) {
+ if (classes == null) {
+ return null;
+ }
+
+ if (classes.length == 0) {
+ return classes;
+ }
+
+ Class>[] convertedClasses = new Class[classes.length];
+ for (int i = 0; i < classes.length; i++) {
+ convertedClasses[i] = primitiveToWrapper(classes[i]);
+ }
+ return convertedClasses;
+ }
+
+ /**
+ * Converts the specified wrapper class to its corresponding primitive
+ * class.
+ *
+ * This method is the counter part of {@code primitiveToWrapper()}.
+ * If the passed in class is a wrapper class for a primitive type, this
+ * primitive type will be returned (e.g. {@code Integer.TYPE} for
+ * {@code Integer.class}). For other classes, or if the parameter is
+ * null, the return value is null.
+ *
+ * @param cls the class to convert, may be null
+ * @return the corresponding primitive type if {@code cls} is a
+ * wrapper class, null otherwise
+ * @see #primitiveToWrapper(Class)
+ * @since 2.4
+ */
+ public static Class> wrapperToPrimitive(Class> cls) {
+ return wrapperPrimitiveMap.get(cls);
+ }
+
+ /**
+ * Converts the specified array of wrapper Class objects to an array of
+ * its corresponding primitive Class objects.
+ *
+ * This method invokes {@code wrapperToPrimitive()} for each element
+ * of the passed in array.
+ *
+ * @param classes the class array to convert, may be null or empty
+ * @return an array which contains for each given class, the primitive class or
+ * null if the original class is not a wrapper class. {@code null} if null input.
+ * Empty array if an empty array passed in.
+ * @see #wrapperToPrimitive(Class)
+ * @since 2.4
+ */
+ public static Class>[] wrappersToPrimitives(Class>... classes) {
+ if (classes == null) {
+ return null;
+ }
+
+ if (classes.length == 0) {
+ return classes;
+ }
+
+ Class>[] convertedClasses = new Class[classes.length];
+ for (int i = 0; i < classes.length; i++) {
+ convertedClasses[i] = wrapperToPrimitive(classes[i]);
+ }
+ return convertedClasses;
+ }
+
+ // Inner class
+ // ----------------------------------------------------------------------
+ /**
+ * Is the specified class an inner class or static nested class.
+ *
+ * @param cls the class to check, may be null
+ * @return {@code true} if the class is an inner or static nested class,
+ * false if not or {@code null}
+ */
+ public static boolean isInnerClass(Class> cls) {
+ return cls != null && cls.getEnclosingClass() != null;
+ }
+
+ // Class loading
+ // ----------------------------------------------------------------------
+ /**
+ * Returns the class represented by {@code className} using the
+ * {@code classLoader}. This implementation supports the syntaxes
+ * "{@code java.util.Map.Entry[]}", "{@code java.util.Map$Entry[]}",
+ * "{@code [Ljava.util.Map.Entry;}", and "{@code [Ljava.util.Map$Entry;}".
+ *
+ * @param classLoader the class loader to use to load the class
+ * @param className the class name
+ * @param initialize whether the class must be initialized
+ * @return the class represented by {@code className} using the {@code classLoader}
+ * @throws ClassNotFoundException if the class is not found
+ */
+ public static Class> getClass(
+ ClassLoader classLoader, String className, boolean initialize) throws ClassNotFoundException {
+ try {
+ Class> clazz;
+ if (abbreviationMap.containsKey(className)) {
+ String clsName = "[" + abbreviationMap.get(className);
+ clazz = Class.forName(clsName, initialize, classLoader).getComponentType();
+ } else {
+ clazz = Class.forName(toCanonicalName(className), initialize, classLoader);
+ }
+ return clazz;
+ } catch (ClassNotFoundException ex) {
+ // allow path separators (.) as inner class name separators
+ int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
+
+ if (lastDotIndex != -1) {
+ try {
+ return getClass(classLoader, className.substring(0, lastDotIndex) +
+ INNER_CLASS_SEPARATOR_CHAR + className.substring(lastDotIndex + 1),
+ initialize);
+ } catch (ClassNotFoundException ex2) { // NOPMD
+ // ignore exception
+ }
+ }
+
+ throw ex;
+ }
+ }
+
+ /**
+ * Returns the (initialized) class represented by {@code className}
+ * using the {@code classLoader}. This implementation supports
+ * the syntaxes "{@code java.util.Map.Entry[]}",
+ * "{@code java.util.Map$Entry[]}", "{@code [Ljava.util.Map.Entry;}",
+ * and "{@code [Ljava.util.Map$Entry;}".
+ *
+ * @param classLoader the class loader to use to load the class
+ * @param className the class name
+ * @return the class represented by {@code className} using the {@code classLoader}
+ * @throws ClassNotFoundException if the class is not found
+ */
+ public static Class> getClass(ClassLoader classLoader, String className) throws ClassNotFoundException {
+ return getClass(classLoader, className, true);
+ }
+
+ /**
+ * Returns the (initialized) class represented by {@code className}
+ * using the current thread's context class loader. This implementation
+ * supports the syntaxes "{@code java.util.Map.Entry[]}",
+ * "{@code java.util.Map$Entry[]}", "{@code [Ljava.util.Map.Entry;}",
+ * and "{@code [Ljava.util.Map$Entry;}".
+ *
+ * @param className the class name
+ * @return the class represented by {@code className} using the current thread's context class loader
+ * @throws ClassNotFoundException if the class is not found
+ */
+ public static Class> getClass(String className) throws ClassNotFoundException {
+ return getClass(className, true);
+ }
+
+ /**
+ * Returns the class represented by {@code className} using the
+ * current thread's context class loader. This implementation supports the
+ * syntaxes "{@code java.util.Map.Entry[]}", "{@code java.util.Map$Entry[]}",
+ * "{@code [Ljava.util.Map.Entry;}", and "{@code [Ljava.util.Map$Entry;}".
+ *
+ * @param className the class name
+ * @param initialize whether the class must be initialized
+ * @return the class represented by {@code className} using the current thread's context class loader
+ * @throws ClassNotFoundException if the class is not found
+ */
+ public static Class> getClass(String className, boolean initialize) throws ClassNotFoundException {
+ ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
+ ClassLoader loader = contextCL == null ? ClassUtils.class.getClassLoader() : contextCL;
+ return getClass(loader, className, initialize);
+ }
+
+
+ // ----------------------------------------------------------------------
+ /**
+ * Converts a class name to a JLS style class name.
+ *
+ * @param className the class name
+ * @return the converted name
+ */
+ private static String toCanonicalName(String className) {
+ className = className == null ? null : className.replaceAll("\\s+", "");
+ if (className == null) {
+ throw new NullPointerException("className must not be null.");
+ } else if (className.endsWith("[]")) {
+ StringBuilder classNameBuffer = new StringBuilder();
+ while (className.endsWith("[]")) {
+ className = className.substring(0, className.length() - 2);
+ classNameBuffer.append("[");
+ }
+ String abbreviation = abbreviationMap.get(className);
+ if (abbreviation != null) {
+ classNameBuffer.append(abbreviation);
+ } else {
+ classNameBuffer.append("L").append(className).append(";");
+ }
+ className = classNameBuffer.toString();
+ }
+ return className;
+ }
+
+ /**
+ * Converts an array of {@code Object} in to an array of {@code Class} objects.
+ * If any of these objects is null, a null element will be inserted into the array.
+ *
+ * This method returns {@code null} for a {@code null} input array.
+ *
+ * @param array an {@code Object} array
+ * @return a {@code Class} array, {@code null} if null array input
+ * @since 2.4
+ */
+ public static Class>[] toClass(Object... array) {
+ if (array == null) {
+ return null;
+ } else if (array.length == 0) {
+ return new Class>[0];
+ }
+ Class>[] classes = new Class[array.length];
+ for (int i = 0; i < array.length; i++) {
+ classes[i] = array[i] == null ? null : array[i].getClass();
+ }
+ return classes;
+ }
+
+ // Short canonical name
+ // ----------------------------------------------------------------------
+ /**
+ * Gets the canonical name minus the package name for an {@code Object}.
+ *
+ * @param object the class to get the short name for, may be null
+ * @param valueIfNull the value to return if null
+ * @return the canonical name of the object without the package name, or the null value
+ * @since 2.4
+ */
+ public static String getShortCanonicalName(Object object, String valueIfNull) {
+ if (object == null) {
+ return valueIfNull;
+ }
+ return getShortCanonicalName(object.getClass().getName());
+ }
+
+ /**
+ * Gets the canonical name minus the package name from a {@code Class}.
+ *
+ * @param cls the class to get the short name for.
+ * @return the canonical name without the package name or an empty string
+ * @since 2.4
+ */
+ public static String getShortCanonicalName(Class> cls) {
+ if (cls == null) {
+ return "";
+ }
+ return getShortCanonicalName(cls.getName());
+ }
+
+ /**
+ * Gets the canonical name minus the package name from a String.
+ *
+ * The string passed in is assumed to be a canonical name - it is not checked.
+ *
+ * @param canonicalName the class name to get the short name for
+ * @return the canonical name of the class without the package name or an empty string
+ * @since 2.4
+ */
+ public static String getShortCanonicalName(String canonicalName) {
+ return ClassUtils.getShortClassName(getCanonicalName(canonicalName));
+ }
+
+ // Package name
+ // ----------------------------------------------------------------------
+ /**
+ * Gets the package name from the canonical name of an {@code Object}.
+ *
+ * @param object the class to get the package name for, may be null
+ * @param valueIfNull the value to return if null
+ * @return the package name of the object, or the null value
+ * @since 2.4
+ */
+ public static String getPackageCanonicalName(Object object, String valueIfNull) {
+ if (object == null) {
+ return valueIfNull;
+ }
+ return getPackageCanonicalName(object.getClass().getName());
+ }
+
+ /**
+ * Gets the package name from the canonical name of a {@code Class}.
+ *
+ * @param cls the class to get the package name for, may be {@code null}.
+ * @return the package name or an empty string
+ * @since 2.4
+ */
+ public static String getPackageCanonicalName(Class> cls) {
+ if (cls == null) {
+ return "";
+ }
+ return getPackageCanonicalName(cls.getName());
+ }
+
+ /**
+ * Gets the package name from the canonical name.
+ *
+ * The string passed in is assumed to be a canonical name - it is not checked.
+ * If the class is unpackaged, return an empty string.
+ *
+ * @param canonicalName the canonical name to get the package name for, may be {@code null}
+ * @return the package name or an empty string
+ * @since 2.4
+ */
+ public static String getPackageCanonicalName(String canonicalName) {
+ return ClassUtils.getPackageName(getCanonicalName(canonicalName));
+ }
+
+ /**
+ * Converts a given name of class into canonical format.
+ * If name of class is not a name of array class it returns
+ * unchanged name.
+ * Example:
+ *
+ * - {@code getCanonicalName("[I") = "int[]"}
+ * - {@code getCanonicalName("[Ljava.lang.String;") = "java.lang.String[]"}
+ * - {@code getCanonicalName("java.lang.String") = "java.lang.String"}
+ *
+ *
+ *
+ * @param className the name of class
+ * @return canonical form of class name
+ * @since 2.4
+ */
+ private static String getCanonicalName(String className) {
+ className = className.replaceAll("\\s", "");
+ if (className == null) {
+ return null;
+ } else {
+ int dim = 0;
+ while (className.startsWith("[")) {
+ dim++;
+ className = className.substring(1);
+ }
+ if (dim < 1) {
+ return className;
+ } else {
+ if (className.startsWith("L")) {
+ className = className.substring(
+ 1,
+ className.endsWith(";")
+ ? className.length() - 1
+ : className.length());
+ } else {
+ if (className.length() > 0) {
+ className = reverseAbbreviationMap.get(className.substring(0, 1));
+ }
+ }
+ StringBuilder canonicalClassNameBuffer = new StringBuilder(className);
+ for (int i = 0; i < dim; i++) {
+ canonicalClassNameBuffer.append("[]");
+ }
+ return canonicalClassNameBuffer.toString();
+ }
+ }
+ }
+
+ public static Set getClassConstantStrings(Class clz) {
+ HashSet strs = new HashSet<>();
+ Field fields[] = clz.getDeclaredFields();
+ for (Field field : fields) {
+ // String类型
+ if (field.getType() != String.class) {
+ continue;
+ }
+ // 字段修饰符必须是public final staic
+ if ((field.getModifiers() & (Modifier.STATIC | Modifier.FINAL | Modifier.PUBLIC)) != (Modifier.STATIC | Modifier.FINAL | Modifier.PUBLIC)) {
+ continue;
+ }
+ field.setAccessible(true);
+ try {
+ strs.add((String) field.get(null));
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ }
+ }
+ return strs;
+ }
+
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/MemberUtils.java b/app/src/main/java/com/example/studyapp/utils/MemberUtils.java
new file mode 100644
index 0000000..8cffcf4
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/utils/MemberUtils.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.studyapp.utils;
+
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Member;
+import java.lang.reflect.Modifier;
+
+/**
+ * Contains common code for working with Methods/Constructors, extracted and
+ * refactored from MethodUtils
when it was imported from Commons
+ * BeanUtils.
+ *
+ * @since 2.5
+ * @version $Id: MemberUtils.java 1143537 2011-07-06 19:30:22Z joehni $
+ */
+public abstract class MemberUtils {
+ // TODO extract an interface to implement compareParameterSets(...)?
+
+ private static final int ACCESS_TEST = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE;
+
+ /** Array of primitive number types ordered by "promotability" */
+ private static final Class>[] ORDERED_PRIMITIVE_TYPES = { Byte.TYPE, Short.TYPE, Character.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE };
+
+ /**
+ * XXX Default access superclass workaround
+ *
+ * When a public class has a default access superclass with public members,
+ * these members are accessible. Calling them from compiled code works fine.
+ * Unfortunately, on some JVMs, using reflection to invoke these members
+ * seems to (wrongly) prevent access even when the modifier is public.
+ * Calling setAccessible(true) solves the problem but will only work from
+ * sufficiently privileged code. Better workarounds would be gratefully
+ * accepted.
+ *
+ * @param o
+ * the AccessibleObject to set as accessible
+ */
+ static void setAccessibleWorkaround(AccessibleObject o) {
+ if (o == null || o.isAccessible()) {
+ return;
+ }
+ Member m = (Member) o;
+ if (Modifier.isPublic(m.getModifiers()) && isPackageAccess(m.getDeclaringClass().getModifiers())) {
+ try {
+ o.setAccessible(true);
+ } catch (SecurityException e) { // NOPMD
+ // ignore in favor of subsequent IllegalAccessException
+ }
+ }
+ }
+
+ /**
+ * Returns whether a given set of modifiers implies package access.
+ *
+ * @param modifiers
+ * to test
+ * @return true unless package/protected/private modifier detected
+ */
+ static boolean isPackageAccess(int modifiers) {
+ return (modifiers & ACCESS_TEST) == 0;
+ }
+
+ /**
+ * Returns whether a Member is accessible.
+ *
+ * @param m
+ * Member to check
+ * @return true if m
is accessible
+ */
+ static boolean isAccessible(Member m) {
+ return m != null && Modifier.isPublic(m.getModifiers()) && !m.isSynthetic();
+ }
+
+ /**
+ * Compares the relative fitness of two sets of parameter types in terms of
+ * matching a third set of runtime parameter types, such that a list ordered
+ * by the results of the comparison would return the best match first
+ * (least).
+ *
+ * @param left
+ * the "left" parameter set
+ * @param right
+ * the "right" parameter set
+ * @param actual
+ * the runtime parameter types to match against left
+ * /right
+ * @return int consistent with compare
semantics
+ */
+ public static int compareParameterTypes(Class>[] left, Class>[] right, Class>[] actual) {
+ float leftCost = getTotalTransformationCost(actual, left);
+ float rightCost = getTotalTransformationCost(actual, right);
+ return leftCost < rightCost ? -1 : rightCost < leftCost ? 1 : 0;
+ }
+
+ /**
+ * Returns the sum of the object transformation cost for each class in the
+ * source argument list.
+ *
+ * @param srcArgs
+ * The source arguments
+ * @param destArgs
+ * The destination arguments
+ * @return The total transformation cost
+ */
+ private static float getTotalTransformationCost(Class>[] srcArgs, Class>[] destArgs) {
+ float totalCost = 0.0f;
+ for (int i = 0; i < srcArgs.length; i++) {
+ Class> srcClass, destClass;
+ srcClass = srcArgs[i];
+ destClass = destArgs[i];
+ totalCost += getObjectTransformationCost(srcClass, destClass);
+ }
+ return totalCost;
+ }
+
+ /**
+ * Gets the number of steps required needed to turn the source class into
+ * the destination class. This represents the number of steps in the object
+ * hierarchy graph.
+ *
+ * @param srcClass
+ * The source class
+ * @param destClass
+ * The destination class
+ * @return The cost of transforming an object
+ */
+ private static float getObjectTransformationCost(Class> srcClass, Class> destClass) {
+ if (destClass.isPrimitive()) {
+ return getPrimitivePromotionCost(srcClass, destClass);
+ }
+ float cost = 0.0f;
+ while (srcClass != null && !destClass.equals(srcClass)) {
+ if (destClass.isInterface() && ClassUtils.isAssignable(srcClass, destClass, true)) {
+ // slight penalty for interface match.
+ // we still want an exact match to override an interface match,
+ // but
+ // an interface match should override anything where we have to
+ // get a superclass.
+ cost += 0.25f;
+ break;
+ }
+ cost++;
+ srcClass = srcClass.getSuperclass();
+ }
+ /*
+ * If the destination class is null, we've travelled all the way up to
+ * an Object match. We'll penalize this by adding 1.5 to the cost.
+ */
+ if (srcClass == null) {
+ cost += 1.5f;
+ }
+ return cost;
+ }
+
+ /**
+ * Gets the number of steps required to promote a primitive number to
+ * another type.
+ *
+ * @param srcClass
+ * the (primitive) source class
+ * @param destClass
+ * the (primitive) destination class
+ * @return The cost of promoting the primitive
+ */
+ private static float getPrimitivePromotionCost(final Class> srcClass, final Class> destClass) {
+ float cost = 0.0f;
+ Class> cls = srcClass;
+ if (!cls.isPrimitive()) {
+ // slight unwrapping penalty
+ cost += 0.1f;
+ cls = ClassUtils.wrapperToPrimitive(cls);
+ }
+ for (int i = 0; cls != destClass && i < ORDERED_PRIMITIVE_TYPES.length; i++) {
+ if (cls == ORDERED_PRIMITIVE_TYPES[i]) {
+ cost += 0.1f;
+ if (i < ORDERED_PRIMITIVE_TYPES.length - 1) {
+ cls = ORDERED_PRIMITIVE_TYPES[i + 1];
+ }
+ }
+ }
+ return cost;
+ }
+
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/ReflectionHelper.java b/app/src/main/java/com/example/studyapp/utils/ReflectionHelper.java
new file mode 100644
index 0000000..8ce1399
--- /dev/null
+++ b/app/src/main/java/com/example/studyapp/utils/ReflectionHelper.java
@@ -0,0 +1,1001 @@
+package com.example.studyapp.utils;
+
+
+import android.util.Log;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.HashMap;
+
+
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public class ReflectionHelper {
+
+ private static final HashMap fieldCache = new HashMap();
+ private static final HashMap methodCache = new HashMap();
+ private static final HashMap> constructorCache = new HashMap>();
+
+ public static boolean hasField(Class> clazz, String fieldName) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append('#');
+ sb.append(fieldName);
+ String fullFieldName = sb.toString();
+
+ if (fieldCache.containsKey(fullFieldName)) {
+ Field field = fieldCache.get(fullFieldName);
+ if (field == null)
+ return false;
+ return true;
+ }
+
+ try {
+ Field field = findFieldRecursiveImpl(clazz, fieldName);
+ field.setAccessible(true);
+ fieldCache.put(fullFieldName, field);
+ return true;
+ } catch (NoSuchFieldException e) {
+ fieldCache.put(fullFieldName, null);
+ return false;
+ }
+ }
+
+ public static Field findField(Class> clazz, String fieldName) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append('#');
+ sb.append(fieldName);
+ String fullFieldName = sb.toString();
+
+ if (fieldCache.containsKey(fullFieldName)) {
+ Field field = fieldCache.get(fullFieldName);
+ if (field == null)
+ throw new NoSuchFieldError(fullFieldName);
+ return field;
+ }
+
+ try {
+ Field field = findFieldRecursiveImpl(clazz, fieldName);
+ field.setAccessible(true);
+ fieldCache.put(fullFieldName, field);
+ return field;
+ } catch (NoSuchFieldException e) {
+ fieldCache.put(fullFieldName, null);
+ throw new NoSuchFieldError(fullFieldName);
+ }
+ }
+
+ private static Field findFieldRecursiveImpl(Class> clazz, String fieldName) throws NoSuchFieldException {
+ try {
+ return clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ while (true) {
+ clazz = clazz.getSuperclass();
+ if (clazz == null || clazz.equals(Object.class))
+ break;
+
+ try {
+ return clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException ignored) {}
+ }
+ throw e;
+ }
+ }
+
+ public static Field getField(Class clz, String field) {
+ try {
+ Field fld = findField(clz, field);
+ if (fld != null)
+ fld.setAccessible(true);
+ return fld;
+ } catch (Exception e) {
+ android.util.Log.e("Error", "Exception occurred", e);
+ return null;
+ }
+ }
+
+ public static Class getClass(String name, ClassLoader cl) {
+ try {
+ return Class.forName(name, false, cl);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static boolean hasClass(String name) {
+ try {
+ Class.forName(name);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static Class getClass(String name) {
+ try {
+ return Class.forName(name);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Class getClass(String name, Class clz) {
+ try {
+ ClassLoader cl = clz.getClassLoader();
+ return Class.forName(name, false, cl);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+
+ public static Object invokeStaticMethod(Class cls, Class[] type, String methodname, Object[] args) {
+ try {
+ Method method = cls.getDeclaredMethod(methodname, type);
+ if (method == null)
+ return null;
+ method.setAccessible(true);
+ return method.invoke(null, args);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Object invokeStaticMethod(String clsName, Class[] type, String methodname, Object[] args) {
+ Class clzz = null;
+ try {
+ clzz = Class.forName(clsName);
+ } catch (Exception e) {
+ clzz = null;
+ }
+ if (clzz == null)
+ return null;
+ return invokeStaticMethod(clzz, type, methodname, args);
+ }
+
+ public static Object invokeNonStaticMethod(Object obj, Class[] type, String methodname, Object[] args) {
+ try {
+ Method method = obj.getClass().getDeclaredMethod(methodname, type);
+ if (method == null)
+ return null;
+ method.setAccessible(true);
+ return method.invoke(obj, args);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static Class>[] getParameterTypes(Object... args) {
+ Class>[] clazzes = new Class>[args.length];
+ for (int i = 0; i < args.length; i++) {
+ clazzes[i] = (args[i] != null) ? args[i].getClass() : null;
+ }
+ return clazzes;
+ }
+
+ private static String getParametersString(Class>... clazzes) {
+ StringBuilder sb = new StringBuilder("(");
+ boolean first = true;
+ for (Class> clazz : clazzes) {
+ if (first)
+ first = false;
+ else
+ sb.append(",");
+
+ if (clazz != null)
+ sb.append(clazz.getCanonicalName());
+ else
+ sb.append("null");
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ public static Constructor> findConstructorExact(Class> clazz, Class>... parameterTypes) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append(getParametersString(parameterTypes));
+ sb.append("#exact");
+ String fullConstructorName = sb.toString();
+
+ if (constructorCache.containsKey(fullConstructorName)) {
+ Constructor> constructor = constructorCache.get(fullConstructorName);
+ if (constructor == null)
+ throw new NoSuchMethodError(fullConstructorName);
+ return constructor;
+ }
+
+ try {
+ Constructor> constructor = clazz.getDeclaredConstructor(parameterTypes);
+ constructor.setAccessible(true);
+ constructorCache.put(fullConstructorName, constructor);
+ return constructor;
+ } catch (NoSuchMethodException e) {
+ constructorCache.put(fullConstructorName, null);
+ throw new NoSuchMethodError(fullConstructorName);
+ }
+ }
+
+ public static Constructor> findConstructorExactThrowNothing(Class> clazz, Class>... parameterTypes) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append(getParametersString(parameterTypes));
+ sb.append("#exact");
+ String fullConstructorName = sb.toString();
+
+ if (constructorCache.containsKey(fullConstructorName)) {
+ Constructor> constructor = constructorCache.get(fullConstructorName);
+ return constructor;
+ }
+
+ try {
+ Constructor> constructor = clazz.getDeclaredConstructor(parameterTypes);
+ constructor.setAccessible(true);
+ constructorCache.put(fullConstructorName, constructor);
+ return constructor;
+ } catch (NoSuchMethodException e) {
+ constructorCache.put(fullConstructorName, null);
+ return null;
+ }
+ }
+
+ public static Constructor> findConstructorBestMatch(Class> clazz, Class>... parameterTypes) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append(getParametersString(parameterTypes));
+ sb.append("#bestmatch");
+ String fullConstructorName = sb.toString();
+
+ if (constructorCache.containsKey(fullConstructorName)) {
+ Constructor> constructor = constructorCache.get(fullConstructorName);
+ if (constructor == null)
+ throw new NoSuchMethodError(fullConstructorName);
+ return constructor;
+ }
+
+ try {
+ Constructor> constructor = findConstructorExact(clazz, parameterTypes);
+ constructorCache.put(fullConstructorName, constructor);
+ return constructor;
+ } catch (NoSuchMethodError ignored) {}
+
+ Constructor> bestMatch = null;
+ Constructor>[] constructors = clazz.getDeclaredConstructors();
+ for (Constructor> constructor : constructors) {
+ // compare name and parameters
+ if (ClassUtils.isAssignable(parameterTypes, constructor.getParameterTypes(), true)) {
+ // get accessible version of method
+ if (bestMatch == null || MemberUtils.compareParameterTypes(
+ constructor.getParameterTypes(),
+ bestMatch.getParameterTypes(),
+ parameterTypes) < 0) {
+ bestMatch = constructor;
+ }
+ }
+ }
+ if (bestMatch != null) {
+ bestMatch.setAccessible(true);
+ constructorCache.put(fullConstructorName, bestMatch);
+ return bestMatch;
+ } else {
+ NoSuchMethodError e = new NoSuchMethodError(fullConstructorName);
+ constructorCache.put(fullConstructorName, null);
+ throw e;
+ }
+ }
+
+ public static Constructor> findConstructorBestMatch(Class> clazz, Object... args) {
+ return findConstructorBestMatch(clazz, getParameterTypes(args));
+ }
+
+ public static Object newInstance(Class> clazz, Object... args) {
+ try {
+ return findConstructorBestMatch(clazz, args).newInstance(args);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static void setObjectField(Object obj, String fieldName, Object value) {
+ try {
+ findField(obj.getClass(), fieldName).set(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setBooleanField(Object obj, String fieldName, boolean value) {
+ try {
+ findField(obj.getClass(), fieldName).setBoolean(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setByteField(Object obj, String fieldName, byte value) {
+ try {
+ findField(obj.getClass(), fieldName).setByte(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setCharField(Object obj, String fieldName, char value) {
+ try {
+ findField(obj.getClass(), fieldName).setChar(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setDoubleField(Object obj, String fieldName, double value) {
+ try {
+ findField(obj.getClass(), fieldName).setDouble(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setFloatField(Object obj, String fieldName, float value) {
+ try {
+ findField(obj.getClass(), fieldName).setFloat(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setIntField(Object obj, String fieldName, int value) {
+ try {
+ findField(obj.getClass(), fieldName).setInt(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setLongField(Object obj, String fieldName, long value) {
+ try {
+ findField(obj.getClass(), fieldName).setLong(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setShortField(Object obj, String fieldName, short value) {
+ try {
+ findField(obj.getClass(), fieldName).setShort(obj, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ //#################################################################################################
+ public static Object getObjectField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).get(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ /** For inner classes, return the "this" reference of the surrounding class */
+ public static Object getSurroundingThis(Object obj) {
+ return getObjectField(obj, "this$0");
+ }
+
+ public static boolean getBooleanField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getBoolean(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static byte getByteField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getByte(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static char getCharField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getChar(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static double getDoubleField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getDouble(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static float getFloatField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getFloat(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static int getStaticIntField(Class clz, String fieldName, int defaultVal) {
+ try {
+ return findField(clz, fieldName).getInt(null);
+ } catch (Exception e) {
+ return defaultVal;
+ }
+ }
+
+ public static int getIntField(Object obj, String fieldName, int defaultVal) {
+ try {
+ return findField(obj.getClass(), fieldName).getInt(obj);
+ } catch (Exception e) {
+ return defaultVal;
+ }
+ }
+
+ public static int getIntField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getInt(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static long getLongField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getLong(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static short getShortField(Object obj, String fieldName) {
+ try {
+ return findField(obj.getClass(), fieldName).getShort(obj);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ //#################################################################################################
+ public static void setStaticObjectField(Class> clazz, String fieldName, Object value) {
+ try {
+ findField(clazz, fieldName).set(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticBooleanField(Class> clazz, String fieldName, boolean value) {
+ try {
+ findField(clazz, fieldName).setBoolean(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticByteField(Class> clazz, String fieldName, byte value) {
+ try {
+ findField(clazz, fieldName).setByte(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticCharField(Class> clazz, String fieldName, char value) {
+ try {
+ findField(clazz, fieldName).setChar(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticDoubleField(Class> clazz, String fieldName, double value) {
+ try {
+ findField(clazz, fieldName).setDouble(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticFloatField(Class> clazz, String fieldName, float value) {
+ try {
+ findField(clazz, fieldName).setFloat(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticIntField(Class> clazz, String fieldName, int value) {
+ try {
+ findField(clazz, fieldName).setInt(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticLongField(Class> clazz, String fieldName, long value) {
+ try {
+ findField(clazz, fieldName).setLong(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static void setStaticShortField(Class> clazz, String fieldName, short value) {
+ try {
+ findField(clazz, fieldName).setShort(null, value);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ //#################################################################################################
+ public static Object getStaticObjectField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).get(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static boolean getStaticBooleanField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getBoolean(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static byte getStaticByteField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getByte(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static char getStaticCharField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getChar(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static double getStaticDoubleField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getDouble(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static float getStaticFloatField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getFloat(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static int getStaticIntField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getInt(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static long getStaticLongField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getLong(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static short getStaticShortField(Class> clazz, String fieldName) {
+ try {
+ return findField(clazz, fieldName).getShort(null);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+ }
+
+ public static Method findMethodExact(Class> clazz, String methodName, Class>... parameterTypes) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append('#');
+ sb.append(methodName);
+ sb.append(getParametersString(parameterTypes));
+ sb.append("#exact");
+ String fullMethodName = sb.toString();
+
+ if (methodCache.containsKey(fullMethodName)) {
+ Method method = methodCache.get(fullMethodName);
+ if (method == null)
+ throw new NoSuchMethodError(fullMethodName);
+ return method;
+ }
+
+ try {
+ Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
+ method.setAccessible(true);
+ methodCache.put(fullMethodName, method);
+ return method;
+ } catch (NoSuchMethodException e) {
+ methodCache.put(fullMethodName, null);
+ throw new NoSuchMethodError(fullMethodName);
+ }
+ }
+
+ public static boolean hasMethod(Class> clazz, String methodName, Class>... parameterTypes) {
+ return findMethodExactThrowNothing(clazz, methodName, parameterTypes) != null;
+ }
+
+ public static Method findMethodExactThrowNothing(Class> clazz, String methodName, Class>... parameterTypes) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append('#');
+ sb.append(methodName);
+ sb.append(getParametersString(parameterTypes));
+ sb.append("#exact");
+ String fullMethodName = sb.toString();
+
+ if (methodCache.containsKey(fullMethodName)) {
+ Method method = methodCache.get(fullMethodName);
+ return method;
+ }
+
+ try {
+ Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
+ method.setAccessible(true);
+ methodCache.put(fullMethodName, method);
+ return method;
+ } catch (NoSuchMethodException e) {
+ methodCache.put(fullMethodName, null);
+ return null;
+ }
+ }
+
+ public static Object getInstance(String className, String staticFieldName) throws Exception {
+ // 获取目标类的 Class 对象
+ Class> clazz = Class.forName(className);
+
+ // 获取指定的静态字段
+ Field field = clazz.getDeclaredField(staticFieldName);
+ field.setAccessible(true); // 如果字段是 private,需要通过反射设置可访问
+
+ // 获取静态字段的值
+ return field.get(null); // 静态字段,使用 null 获取值
+ }
+
+
+ public static Method findMethodBestMatch(Class> clazz, String methodName, Class>... parameterTypes) {
+ StringBuilder sb = new StringBuilder(clazz.getName());
+ sb.append('#');
+ sb.append(methodName);
+ sb.append(getParametersString(parameterTypes));
+ sb.append("#bestmatch");
+ String fullMethodName = sb.toString();
+
+ if (methodCache.containsKey(fullMethodName)) {
+ Method method = methodCache.get(fullMethodName);
+ if (method == null)
+ throw new NoSuchMethodError(fullMethodName);
+ return method;
+ }
+
+ try {
+ Method method = findMethodExact(clazz, methodName, parameterTypes);
+ methodCache.put(fullMethodName, method);
+ return method;
+ } catch (NoSuchMethodError ignored) {}
+
+ Method bestMatch = null;
+ Class> clz = clazz;
+ boolean considerPrivateMethods = true;
+ do {
+ for (Method method : clz.getDeclaredMethods()) {
+ // don't consider private methods of superclasses
+ if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers()))
+ continue;
+
+ // compare name and parameters
+ if (method.getName().equals(methodName) && ClassUtils.isAssignable(parameterTypes, method.getParameterTypes(), true)) {
+ // get accessible version of method
+ if (bestMatch == null || MemberUtils.compareParameterTypes(
+ method.getParameterTypes(),
+ bestMatch.getParameterTypes(),
+ parameterTypes) < 0) {
+ bestMatch = method;
+ }
+ }
+ }
+ considerPrivateMethods = false;
+ } while ((clz = clz.getSuperclass()) != null);
+
+ if (bestMatch != null) {
+ bestMatch.setAccessible(true);
+ methodCache.put(fullMethodName, bestMatch);
+ return bestMatch;
+ } else {
+ NoSuchMethodError e = new NoSuchMethodError(fullMethodName);
+ methodCache.put(fullMethodName, null);
+ throw e;
+ }
+ }
+
+ public static Method findMethodBestMatch(Class> clazz, String methodName, Object... args) {
+ return findMethodBestMatch(clazz, methodName, getParameterTypes(args));
+ }
+
+ public static Object callMethod(Object obj, String methodName, Object... args) {
+ try {
+ return findMethodBestMatch(obj.getClass(), methodName, args).invoke(obj, args);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (InvocationTargetException e) {
+ throw new InvocationTargetError(e.getCause());
+ }
+ }
+
+ /**
+ * Call instance or static method methodName
for object obj
with the arguments
+ * args
. The types for the arguments will be taken from parameterTypes
.
+ * This array can have items that are null
. In this case, the type for this parameter
+ * is determined from args
.
+ */
+ public static Object callMethod(Object obj, String methodName, Class>[] parameterTypes, Object... args) {
+ try {
+ return findMethodBestMatch(obj.getClass(), methodName, parameterTypes, args).invoke(obj, args);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (InvocationTargetException e) {
+ throw new InvocationTargetError(e.getCause());
+ }
+ }
+
+ /**
+ * Call static method methodName
for class clazz
with the arguments
+ * args
. The types for the arguments will be determined automaticall from args
+ */
+ public static Object callStaticMethod(Class> clazz, String methodName, Object... args) {
+ try {
+ return findMethodBestMatch(clazz, methodName, args).invoke(null, args);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (InvocationTargetException e) {
+ throw new InvocationTargetError(e.getCause());
+ }
+ }
+
+ /**
+ * Call static method methodName
for class clazz
with the arguments
+ * args
. The types for the arguments will be taken from parameterTypes
.
+ * This array can have items that are null
. In this case, the type for this parameter
+ * is determined from args
.
+ */
+ public static Object callStaticMethod(Class> clazz, String methodName, Class>[] parameterTypes, Object... args) {
+ try {
+ return findMethodBestMatch(clazz, methodName, parameterTypes, args).invoke(null, args);
+ } catch (IllegalAccessException e) {
+ // should not happen
+ e.printStackTrace();
+ throw new IllegalAccessError(e.getMessage());
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (InvocationTargetException e) {
+ throw new InvocationTargetError(e.getCause());
+ }
+ }
+
+ public static class InvocationTargetError extends Error {
+ private static final long serialVersionUID = -1070936889459514628L;
+ public InvocationTargetError(Throwable cause) {
+ super(cause);
+ }
+ public InvocationTargetError(String detailMessage, Throwable cause) {
+ super(detailMessage, cause);
+ }
+ }
+
+ public static String getMethodParams(Class targetClass, String methodName){
+ StringBuffer msg=new StringBuffer();
+ Method[] methods=targetClass.getDeclaredMethods();
+
+ for(Method method:methods){
+ if(methodName.equals(method.getName())){
+ msg.append(Arrays.toString(method.getParameterTypes()));
+ }
+ }
+ return msg.toString();
+ }
+
+ public static String getConstructorParams(Class targetClass){
+ StringBuffer msg=new StringBuffer();
+ Constructor[] constructors=targetClass.getDeclaredConstructors();
+ for(Constructor constructor:constructors){
+ msg.append(Arrays.toString(constructor.getParameterTypes()));
+ }
+ return msg.toString();
+ }
+
+ public static Class[] getMethodParamsForNoOverloading(Class targetClass, String methodName){
+ Class[] result=null;
+ Method[] methods=targetClass.getDeclaredMethods();
+ for(Method method:methods){
+ if(methodName.equals(method.getName())){
+ result=method.getParameterTypes();
+ break;
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/app/src/main/java/com/example/studyapp/utils/ShellUtils.java b/app/src/main/java/com/example/studyapp/utils/ShellUtils.java
index aefc893..d8e7c4e 100644
--- a/app/src/main/java/com/example/studyapp/utils/ShellUtils.java
+++ b/app/src/main/java/com/example/studyapp/utils/ShellUtils.java
@@ -1,6 +1,9 @@
package com.example.studyapp.utils;
+import java.io.BufferedOutputStream;
import java.io.BufferedReader;
+
+import android.os.Build;
import android.util.Log;
import java.io.File;
import java.io.IOException;
@@ -10,6 +13,7 @@ import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
public class ShellUtils {
@@ -36,36 +40,56 @@ public class ShellUtils {
}
public static boolean hasBin(String binName) {
- if (!binName.matches("^[a-zA-Z0-9._-]+$")) {
- throw new IllegalArgumentException("Invalid bin name");
+ // 验证 binName 是否符合规则
+ if (binName == null || binName.isEmpty()) {
+ throw new IllegalArgumentException("Bin name cannot be null or empty");
}
- String[] paths = System.getenv("PATH").split(":");
+ for (char c : binName.toCharArray()) {
+ if (!Character.isLetterOrDigit(c) && c != '.' && c != '_' && c != '-') {
+ throw new IllegalArgumentException("Invalid bin name");
+ }
+ }
+
+ // 获取 PATH 环境变量
+ String pathEnv = System.getenv("PATH");
+ if (pathEnv == null) {
+ Log.e("hasBin", "PATH environment variable is not available");
+ return false;
+ }
+
+ // 使用适合当前系统的路径分隔符分割路径
+ String[] paths = pathEnv.split(File.pathSeparator);
for (String path : paths) {
- File file = new File(path + File.separator + binName);
+ File file = new File(path, binName); // 使用 File 构造完整路径
try {
- if (file.exists() && file.canExecute()) {
+ // 检查文件是否可执行
+ if (file.canExecute()) {
return true;
}
} catch (SecurityException e) {
- Log.e("hasBin", "Security exception occurred: " + e.getMessage());
+ Log.e("hasBin", "Security exception occurred while checking: " + file.getAbsolutePath(), e);
}
}
+
+ // 如果未找到可执行文件,返回 false
return false;
}
- public static String execRootCmdAndGetResult(String cmd){
+ public static String execRootCmdAndGetResult(String cmd) {
if (cmd == null || cmd.trim().isEmpty()) {
Log.e("ShellUtils", "Unsafe or empty command. Aborting execution.");
throw new IllegalArgumentException("Unsafe or empty command.");
}
+ if (!isCommandSafe(cmd)) { // 检查命令的合法性
+ throw new IllegalArgumentException("Detected unsafe command.");
+ }
+
Process process = null;
- OutputStream os = null;
- BufferedReader br = null;
try {
+ // 初始化并选择 shell
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");
@@ -75,54 +99,61 @@ public class ShellUtils {
process = Runtime.getRuntime().exec("sh");
}
- os = process.getOutputStream();
- os.write((cmd + "\n").getBytes());
- os.write(("exit\n").getBytes());
- os.flush();
+ // 使用 try-with-resources 关闭流
+ try (OutputStream os = new BufferedOutputStream(process.getOutputStream());
+ InputStream is = process.getInputStream();
+ InputStream errorStream = process.getErrorStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream))) {
- // 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);
+ // 异步处理错误流
+ Thread errorThread = new Thread(() -> errorReader.lines().forEach(line -> Log.e("ShellUtils", "Shell Error: " + line)));
+ errorThread.start();
+
+ // 写入命令到 shell
+ os.write((cmd + "\n").getBytes());
+ os.write("exit\n".getBytes());
+ os.flush();
+
+ // 等待 process 执行完成
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (!process.waitFor(10, TimeUnit.SECONDS)) {
+ process.destroy();
+ throw new RuntimeException("Shell command execution timeout.");
+ }
+ } else {
+ long startTime = System.currentTimeMillis();
+ while (true) {
+ try {
+ process.exitValue();
+ break;
+ } catch (IllegalThreadStateException e) {
+ if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds
+ process.destroy();
+ throw new RuntimeException("Shell command execution timeout.");
+ }
+ Thread.sleep(100); // Sleep briefly before re-checking
+ }
}
- } 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");
+ // 读取输出流中的结果
+ StringBuilder output = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ output.append(line).append("\n");
+ }
+ return output.toString().trim();
}
- return sb.toString();
- } catch (Exception e) {
- e.printStackTrace();
+ } catch (IOException | InterruptedException e) {
+ Log.e("ShellUtils", "Command execution failed: " + e.getMessage());
+ Thread.currentThread().interrupt(); // 恢复中断状态
+ return "Error: " + e.getMessage();
} 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) {
@@ -130,13 +161,46 @@ public class ShellUtils {
Log.e("ShellUtils", "Unsafe command, aborting.");
return;
}
+
List cmds = new ArrayList<>();
cmds.add(cmd);
- execRootCmds(cmds);
+
+ try {
+ // 使用同步块确保线程安全
+ synchronized (ShellUtils.class) {
+ List results = execRootCmds(cmds);
+ for (String result : results) {
+ Log.d("ShellUtils", "Command Result: " + result); // 可根据生产环境禁用日志
+ }
+ }
+ } catch (Exception e) {
+ Log.e("ShellUtils", "Error executing root command: ", e);
+ }
}
private static boolean isCommandSafe(String cmd) {
- return cmd.matches("^[a-zA-Z0-9._/:\\- ]+$");
+ // 空和空字符串验证
+ if (cmd == null || cmd.trim().isEmpty()) {
+ return false;
+ }
+
+ // 仅允许安全字符
+ if (!cmd.matches("^[a-zA-Z0-9._/:\\- ]+$")) {
+ return false;
+ }
+
+ // 添加更多逻辑检查,根据预期命令规则
+ // 例如:禁止多重命令,检查逻辑运算符 "&&" 或 "||"
+ if (cmd.contains("&&") || cmd.contains("||")) {
+ return false;
+ }
+
+ // 检查命令长度,避免过长命令用于攻击
+ if (cmd.length() > 500) { // 假定命令应当限制在 500 字符以内
+ return false;
+ }
+
+ return true;
}
public static List execRootCmds(List cmds) {
@@ -145,25 +209,63 @@ public class ShellUtils {
try {
// 初始化 Shell 环境
process = hasBin("su") ? Runtime.getRuntime().exec("su") : Runtime.getRuntime().exec("sh");
- try (OutputStream os = process.getOutputStream();
- InputStream is = process.getInputStream();
- BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
+
+ // 启动读取线程
+ Process stdProcess = process;
+ Thread stdThread = new Thread(() -> {
+ try (BufferedReader stdReader = new BufferedReader(new InputStreamReader(stdProcess.getInputStream()))) {
+ List localResults = new ArrayList<>();
+ String line;
+ while ((line = stdReader.readLine()) != null) {
+ localResults.add(line);
+ Log.d("ShellUtils", "Stdout: " + line);
+ }
+ synchronized (results) {
+ results.addAll(localResults);
+ }
+ } catch (IOException ioException) {
+ Log.e("ShellUtils", "Error reading stdout", ioException);
+ }
+ });
+
+ Process finalProcess = process;
+ Thread errThread = new Thread(() -> {
+ try (BufferedReader errReader = new BufferedReader(new InputStreamReader(finalProcess.getErrorStream()))) {
+ String line;
+ while ((line = errReader.readLine()) != null) {
+ Log.e("ShellUtils", "Stderr: " + line);
+ }
+ } catch (IOException ioException) {
+ Log.e("ShellUtils", "Error reading stderr", ioException);
+ }
+ });
+
+ // 启动子线程
+ stdThread.start();
+ errThread.start();
+
+ try (OutputStream os = process.getOutputStream()) {
for (String cmd : cmds) {
- Log.d("ShellUtils", "Executing command: " + cmd);
+ if (!isCommandSafe(cmd)) {
+ Log.w("ShellUtils", "Skipping unsafe command: " + cmd);
+ continue;
+ }
os.write((cmd + "\n").getBytes());
+ Log.d("ShellUtils", "Executing command: " + cmd);
}
os.write("exit\n".getBytes());
os.flush();
- process.waitFor();
- // 获取命令输出结果
- String line;
- while ((line = reader.readLine()) != null) {
- results.add(line);
- Log.d("ShellUtils", "Command output: " + line);
- }
}
+
+ // 等待子进程完成
+ process.waitFor();
+
+ // 等待子线程完成
+ stdThread.join();
+ errThread.join();
+
} catch (Exception e) {
- Log.e("ShellUtils", "Error executing commands: " + e.getMessage());
+ Log.e("ShellUtils", "Error executing commands", e);
} finally {
if (process != null) {
process.destroy();
@@ -171,4 +273,30 @@ public class ShellUtils {
}
return results;
}
+
+ public static boolean hasRootAccess() {
+ // 记录是否出现安全异常
+ boolean hasSecurityError = false;
+
+ // 检查二进制文件
+ String[] binariesToCheck = {"su", "xu", "vu"};
+ for (String bin : binariesToCheck) {
+ try {
+ if (hasBin(bin)) {
+ return true;
+ }
+ } catch (SecurityException e) {
+ hasSecurityError = true;
+ Log.e("ShellUtils", "Security exception while checking: " + bin, e);
+ }
+ }
+
+ // 判断如果发生安全异常则反馈问题
+ if (hasSecurityError) {
+ Log.w("ShellUtils", "Potential security error detected while checking root access.");
+ }
+
+ // 没有找到合法的二进制文件,则认为无root权限
+ return false;
+ }
}
diff --git a/app/src/main/java/com/example/studyapp/utils/V2rayUtil.java b/app/src/main/java/com/example/studyapp/utils/V2rayUtil.java
index ac93430..67abe06 100644
--- a/app/src/main/java/com/example/studyapp/utils/V2rayUtil.java
+++ b/app/src/main/java/com/example/studyapp/utils/V2rayUtil.java
@@ -4,10 +4,12 @@ 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;
@@ -108,4 +110,67 @@ public class V2rayUtil {
}
}
}
+
+ 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;
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 23d75da..79326d0 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -3,19 +3,46 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:orientation="vertical"
- android:layout_gravity="center"
- tools:context=".MainActivity"
+ android:gravity="center"
android:padding="16dp"
- android:background="@drawable/background">
+ android:background="@drawable/background"
+ tools:context=".MainActivity">
+ android:text="开始运行脚本"
+ android:contentDescription="运行脚本按钮" />
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/err.log b/err.log
index 6d84278..44e5434 100644
--- a/err.log
+++ b/err.log
@@ -1,45 +1,106 @@
-2025-05-27 19:04:08.548 301-8923 Vpn system_server I Established by com.example.studyapp on tun0
-2025-05-27 19:04:08.551 41957-42156 CustomVpnService com.example.studyapp D Packet Info: TCP=false, DNS=false, Length=76
-2025-05-27 19:04:08.553 933-933 GoogleInpu...hodService com...gle.android.inputmethod.latin I GoogleInputMethodService.onStartInput():1247 onStartInput(EditorInfo{EditorInfo{packageName=com.example.studyapp, inputType=0, inputTypeString=NULL, enableLearning=false, autoCorrection=false, autoComplete=false, imeOptions=0, privateImeOptions=null, actionName=UNSPECIFIED, actionLabel=null, initialSelStart=-1, initialSelEnd=-1, initialCapsMode=0, label=null, fieldId=-1, fieldName=null, extras=null, hintText=null, hintLocales=[]}}, false)
-2025-05-27 19:04:08.557 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 1 (Ask Gemini)
- java.io.IOException: Stream Closed
- at java.io.FileInputStream.read(FileInputStream.java:316)
- at java.io.FileInputStream.read(FileInputStream.java:292)
- at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
- at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
- at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
- at java.lang.Thread.run(Thread.java:1012)
-2025-05-27 19:04:08.557 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 2 (Ask Gemini)
- java.io.IOException: Stream Closed
- at java.io.FileInputStream.read(FileInputStream.java:316)
- at java.io.FileInputStream.read(FileInputStream.java:292)
- at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
- at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
- at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
- at java.lang.Thread.run(Thread.java:1012)
-2025-05-27 19:04:08.557 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 3 (Ask Gemini)
- java.io.IOException: Stream Closed
- at java.io.FileInputStream.read(FileInputStream.java:316)
- at java.io.FileInputStream.read(FileInputStream.java:292)
- at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
- at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
- at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
- at java.lang.Thread.run(Thread.java:1012)
-2025-05-27 19:04:08.558 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 4 (Ask Gemini)
- java.io.IOException: Stream Closed
- at java.io.FileInputStream.read(FileInputStream.java:316)
- at java.io.FileInputStream.read(FileInputStream.java:292)
- at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
- at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
- at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
- at java.lang.Thread.run(Thread.java:1012)
-2025-05-27 19:04:08.558 41957-42156 CustomVpnService com.example.studyapp E Error reading packet. Retry attempt 5 (Ask Gemini)
- java.io.IOException: Stream Closed
- at java.io.FileInputStream.read(FileInputStream.java:316)
- at java.io.FileInputStream.read(FileInputStream.java:292)
- at com.example.studyapp.proxy.CustomVpnService.handleVpnTraffic(CustomVpnService.java:192)
- at com.example.studyapp.proxy.CustomVpnService.lambda$startVpn$0$com-example-studyapp-proxy-CustomVpnService(CustomVpnService.java:113)
- at com.example.studyapp.proxy.CustomVpnService$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
- at java.lang.Thread.run(Thread.java:1012)
-2025-05-27 19:04:08.558 41957-42156 CustomVpnService com.example.studyapp E Max retry reached. Exiting loop.
-2025-05-27 19:04:08.582 301-1568 WindowManager system_server V getPackagePerformanceMode -- ComponentInfo{com.example.studyapp/com.example.studyapp.MainActivity} -- com.example.studyapp -- mode=0
\ No newline at end of file
+2025-05-29 15:19:05.558 65246-65246 AndroidRuntime com.example.studyapp D Shutting down VM
+2025-05-29 15:19:05.559 65246-65246 AndroidRuntime com.example.studyapp E FATAL EXCEPTION: main (Ask Gemini)
+ Process: com.example.studyapp, PID: 65246
+ java.lang.IllegalArgumentException: Service not registered: com.example.studyapp.MainActivity$1@992d8bc
+ at android.app.LoadedApk.forgetServiceDispatcher(LoadedApk.java:2015)
+ at android.app.ContextImpl.unbindService(ContextImpl.java:2076)
+ at android.content.ContextWrapper.unbindService(ContextWrapper.java:889)
+ at android.content.ContextWrapper.unbindService(ContextWrapper.java:889)
+ at com.example.studyapp.MainActivity.stopProxy(MainActivity.java:186)
+ at com.example.studyapp.MainActivity.lambda$onCreate$2$com-example-studyapp-MainActivity(MainActivity.java:94)
+ at com.example.studyapp.MainActivity$$ExternalSyntheticLambda5.onClick(D8$$SyntheticClass:0)
+ at android.view.View.performClick(View.java:7542)
+ at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1218)
+ at android.view.View.performClickInternal(View.java:7519)
+ at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)
+ at android.view.View$PerformClick.run(View.java:29476)
+ at android.os.Handler.handleCallback(Handler.java:942)
+ at android.os.Handler.dispatchMessage(Handler.java:99)
+ at android.os.Looper.loopOnce(Looper.java:201)
+ at android.os.Looper.loop(Looper.java:288)
+ at android.app.ActivityThread.main(ActivityThread.java:7930)
+ at java.lang.reflect.Method.invoke(Native Method)
+ at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
+ at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
+2025-05-29 15:19:05.560 301-56098 ActivityTaskManager system_server W Force finishing activity com.example.studyapp/.MainActivity
+2025-05-29 15:19:05.566 65246-65246 Process com.example.studyapp I Sending signal. PID: 65246 SIG: 9
+---------------------------- PROCESS ENDED (65246) for package com.example.studyapp ----------------------------
+2025-05-29 15:19:05.600 301-317 ActivityManager system_server W Exception when unbinding service com.example.studyapp/.proxy.CustomVpnService (Ask Gemini)
+ android.os.DeadObjectException
+ at android.os.BinderProxy.transactNative(Native Method)
+ at android.os.BinderProxy.transact(BinderProxy.java:584)
+ at android.app.IApplicationThread$Stub$Proxy.scheduleUnbindService(IApplicationThread.java:1346)
+ at com.android.server.am.ActiveServices.removeConnectionLocked(ActiveServices.java:4923)
+ at com.android.server.am.ActiveServices.unbindServiceLocked(ActiveServices.java:3173)
+ at com.android.server.am.ActivityManagerService.unbindService(ActivityManagerService.java:12798)
+ at android.app.ContextImpl.unbindService(ContextImpl.java:2079)
+ at com.android.server.connectivity.Vpn$1.interfaceRemoved(Vpn.java:2076)
+ at com.android.server.NetworkManagementService.lambda$notifyInterfaceRemoved$3(NetworkManagementService.java:353)
+ at com.android.server.NetworkManagementService$$ExternalSyntheticLambda6.sendCallback(Unknown Source:2)
+ at com.android.server.NetworkManagementService.invokeForAllObservers(NetworkManagementService.java:314)
+ at com.android.server.NetworkManagementService.notifyInterfaceRemoved(NetworkManagementService.java:353)
+ at com.android.server.NetworkManagementService.-$$Nest$mnotifyInterfaceRemoved(Unknown Source:0)
+ at com.android.server.NetworkManagementService$NetdUnsolicitedEventListener.lambda$onInterfaceRemoved$6$com-android-server-NetworkManagementService$NetdUnsolicitedEventListener(NetworkManagementService.java:620)
+ at com.android.server.NetworkManagementService$NetdUnsolicitedEventListener$$ExternalSyntheticLambda0.run(Unknown Source:4)
+ at android.os.Handler.handleCallback(Handler.java:942)
+ at android.os.Handler.dispatchMessage(Handler.java:99)
+ at android.os.Looper.loopOnce(Looper.java:201)
+ at android.os.Looper.loop(Looper.java:288)
+ at android.os.HandlerThread.run(HandlerThread.java:67)
+ at com.android.server.ServiceThread.run(ServiceThread.java:44)
+2025-05-29 15:19:05.600 301-317 ActivityManager system_server W Bringing down service while still waiting for start foreground: ServiceRecord{7871f79 u0 com.example.studyapp/.proxy.CustomVpnService}
+2025-05-29 15:19:05.623 301-326 ActivityManager system_server W Failed to update http proxy for: com.example.studyapp
+2025-05-29 15:19:05.637 301-1242 ActivityManager system_server I app.info.packageName:com.example.studyapp app.setKilled(true)
+2025-05-29 15:19:05.637 301-1242 ActivityManager system_server I Process com.example.studyapp (pid 65246) has died: fg +50 TOP
+2025-05-29 15:19:05.637 301-986 WindowManager system_server I WIN DEATH: Window{d550fa0 u0 com.example.studyapp/com.example.studyapp.MainActivity}
+2025-05-29 15:19:05.637 301-986 InputManager-JNI system_server W Input channel object 'd550fa0 com.example.studyapp/com.example.studyapp.MainActivity (client)' was disposed without first being removed with the input manager!
+2025-05-29 15:19:05.660 301-321 WindowManager system_server W Failed to deliver inset state change to w=Window{d550fa0 u0 com.example.studyapp/com.example.studyapp.MainActivity EXITING} (Ask Gemini)
+ android.os.DeadObjectException
+ at android.os.BinderProxy.transactNative(Native Method)
+ at android.os.BinderProxy.transact(BinderProxy.java:584)
+ at android.view.IWindow$Stub$Proxy.insetsControlChanged(IWindow.java:473)
+ at com.android.server.wm.WindowState.notifyInsetsControlChanged(WindowState.java:4015)
+ at com.android.server.wm.InsetsStateController.lambda$notifyPendingInsetsControlChanged$4$com-android-server-wm-InsetsStateController(InsetsStateController.java:351)
+ at com.android.server.wm.InsetsStateController$$ExternalSyntheticLambda2.run(Unknown Source:2)
+ at com.android.server.wm.WindowAnimator.executeAfterPrepareSurfacesRunnables(WindowAnimator.java:345)
+ at com.android.server.wm.RootWindowContainer.performSurfacePlacementNoTrace(RootWindowContainer.java:832)
+ at com.android.server.wm.RootWindowContainer.performSurfacePlacement(RootWindowContainer.java:777)
+ at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacementLoop(WindowSurfacePlacer.java:177)
+ at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacement(WindowSurfacePlacer.java:126)
+ at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacement(WindowSurfacePlacer.java:115)
+ at com.android.server.wm.WindowSurfacePlacer$Traverser.run(WindowSurfacePlacer.java:57)
+ at android.os.Handler.handleCallback(Handler.java:942)
+ at android.os.Handler.dispatchMessage(Handler.java:99)
+ at android.os.Looper.loopOnce(Looper.java:201)
+ at android.os.Looper.loop(Looper.java:288)
+ at android.os.HandlerThread.run(HandlerThread.java:67)
+ at com.android.server.ServiceThread.run(ServiceThread.java:44)
+2025-05-29 15:19:05.661 301-321 WindowManager system_server W Exception thrown during dispatchAppVisibility Window{d550fa0 u0 com.example.studyapp/com.example.studyapp.MainActivity EXITING} (Ask Gemini)
+ android.os.DeadObjectException
+ at android.os.BinderProxy.transactNative(Native Method)
+ at android.os.BinderProxy.transact(BinderProxy.java:584)
+ at android.view.IWindow$Stub$Proxy.dispatchAppVisibility(IWindow.java:536)
+ at com.android.server.wm.WindowState.sendAppVisibilityToClients(WindowState.java:3478)
+ at com.android.server.wm.WindowContainer.sendAppVisibilityToClients(WindowContainer.java:1234)
+ at com.android.server.wm.WindowToken.setClientVisible(WindowToken.java:392)
+ at com.android.server.wm.ActivityRecord.setClientVisible(ActivityRecord.java:6865)
+ at com.android.server.wm.ActivityRecord.onAnimationFinished(ActivityRecord.java:7687)
+ at com.android.server.wm.ActivityRecord.postApplyAnimation(ActivityRecord.java:5512)
+ at com.android.server.wm.ActivityRecord.commitVisibility(ActivityRecord.java:5472)
+ at com.android.server.wm.ActivityRecord.commitVisibility(ActivityRecord.java:5476)
+ at com.android.server.wm.AppTransitionController.handleClosingApps(AppTransitionController.java:1194)
+ at com.android.server.wm.AppTransitionController.handleAppTransitionReady(AppTransitionController.java:304)
+ at com.android.server.wm.RootWindowContainer.checkAppTransitionReady(RootWindowContainer.java:970)
+ at com.android.server.wm.RootWindowContainer.performSurfacePlacementNoTrace(RootWindowContainer.java:834)
+ at com.android.server.wm.RootWindowContainer.performSurfacePlacement(RootWindowContainer.java:777)
+ at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacementLoop(WindowSurfacePlacer.java:177)
+ at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacement(WindowSurfacePlacer.java:126)
+ at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacement(WindowSurfacePlacer.java:115)
+ at com.android.server.wm.WindowSurfacePlacer$Traverser.run(WindowSurfacePlacer.java:57)
+ at android.os.Handler.handleCallback(Handler.java:942)
+ at android.os.Handler.dispatchMessage(Handler.java:99)
+ at android.os.Looper.loopOnce(Looper.java:201)
+ at android.os.Looper.loop(Looper.java:288)
+ at android.os.HandlerThread.run(HandlerThread.java:67)
+ at com.android.server.ServiceThread.run(ServiceThread.java:44)
+2025-05-29 15:19:06.061 301-320 ActivityTaskManager system_server W Activity top resumed state loss timeout for ActivityRecord{e2942a u0 com.example.studyapp/.MainActivity} t-1 f}}