Compare commits
7 Commits
appbase-v1
...
appbase-v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 32ee7c8845 | |||
| 6e34ee73e9 | |||
| 7eed7357f0 | |||
| d20192cb36 | |||
| 5846784940 | |||
| ef64d6a317 | |||
| 8b2a8328eb |
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Tue Jan 20 21:17:40 HKT 2026
|
#Fri Jan 23 03:11:07 HKT 2026
|
||||||
stageCount=7
|
stageCount=8
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.6
|
publishVersion=15.15.7
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.15.7
|
baseBetaVersion=15.15.8
|
||||||
|
|||||||
@@ -21,5 +21,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// 网络连接类库
|
||||||
|
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||||
|
// Gson
|
||||||
|
api 'com.google.code.gson:gson:2.8.9'
|
||||||
|
// Html 解析
|
||||||
|
api 'org.jsoup:jsoup:1.13.1'
|
||||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Tue Jan 20 21:17:40 HKT 2026
|
#Fri Jan 23 03:11:07 HKT 2026
|
||||||
stageCount=7
|
stageCount=8
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.6
|
publishVersion=15.15.7
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.15.7
|
baseBetaVersion=15.15.8
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="cc.winboll.studio.libappbase">
|
package="cc.winboll.studio.libappbase">
|
||||||
|
|
||||||
<application>
|
<!-- 拥有完全的网络访问权限 -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".CrashHandler$CrashActivity"
|
android:name=".CrashHandler$CrashActivity"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cc.winboll.studio.libappbase;
|
|||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.PackageManager.NameNotFoundException;
|
import android.content.pm.PackageManager.NameNotFoundException;
|
||||||
@@ -26,6 +27,12 @@ public class GlobalApplication extends Application {
|
|||||||
*/
|
*/
|
||||||
private static volatile boolean isDebugging = false;
|
private static volatile boolean isDebugging = false;
|
||||||
|
|
||||||
|
// 新增:WinBoLL 服务器主机地址(volatile 保证多线程可见性)
|
||||||
|
private static volatile String winbollHost = null;
|
||||||
|
// 新增:SP 存储相关常量(私有存储,仅当前应用可访问)
|
||||||
|
private static final String SP_NAME = "WinBoLL_SP_CONFIG";
|
||||||
|
private static final String SP_KEY_WINBOLL_HOST = "winboll_host";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取全局 Application 单例实例(外部可通过此方法获取上下文)
|
* 获取全局 Application 单例实例(外部可通过此方法获取上下文)
|
||||||
* @return GlobalApplication 单例(未初始化时返回 null,需确保配置 AndroidManifest)
|
* @return GlobalApplication 单例(未初始化时返回 null,需确保配置 AndroidManifest)
|
||||||
@@ -76,6 +83,42 @@ public class GlobalApplication extends Application {
|
|||||||
return isDebugging;
|
return isDebugging;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增:设置 WinBoLL 服务器主机地址(同时保存到 SP 持久化)
|
||||||
|
public static void setWinbollHost(String host) {
|
||||||
|
if (sInstance == null) {
|
||||||
|
LogUtils.e(TAG, "setWinbollHost: 应用未初始化,设置失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检查并补全末尾 / 核心改动
|
||||||
|
if (host != null && !host.isEmpty() && !host.endsWith("/")) {
|
||||||
|
host += "/";
|
||||||
|
}
|
||||||
|
// 更新内存中的字段
|
||||||
|
winbollHost = host;
|
||||||
|
// 保存到 SP 持久化(私有模式,安全)
|
||||||
|
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||||
|
sp.edit().putString(SP_KEY_WINBOLL_HOST, host).apply();
|
||||||
|
LogUtils.d(TAG, "setWinbollHost: 服务器地址已设置并持久化,host=" + host);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 新增:获取 WinBoLL 服务器主机地址(优先内存,内存为空则从 SP 读取)
|
||||||
|
public static String getWinbollHost() {
|
||||||
|
if (winbollHost != null) {
|
||||||
|
// 内存中存在,直接返回(提高效率)
|
||||||
|
return winbollHost;
|
||||||
|
}
|
||||||
|
if (sInstance == null) {
|
||||||
|
LogUtils.e(TAG, "getWinbollHost: 应用未初始化,获取失败");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 内存中不存在,从 SP 读取并更新到内存
|
||||||
|
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||||
|
winbollHost = sp.getString(SP_KEY_WINBOLL_HOST, "https://console.winboll.cc/");
|
||||||
|
LogUtils.d(TAG, "getWinbollHost: 从 SP 读取服务器地址,host=" + winbollHost);
|
||||||
|
return winbollHost;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用启动时初始化(仅执行一次)
|
* 应用启动时初始化(仅执行一次)
|
||||||
* 初始化核心框架、恢复调试状态、配置全局异常处理等
|
* 初始化核心框架、恢复调试状态、配置全局异常处理等
|
||||||
@@ -86,11 +129,12 @@ public class GlobalApplication extends Application {
|
|||||||
// 初始化单例实例(确保在所有初始化操作前完成)
|
// 初始化单例实例(确保在所有初始化操作前完成)
|
||||||
sInstance = this;
|
sInstance = this;
|
||||||
|
|
||||||
|
|
||||||
// 初始化基础组件(日志、崩溃处理、Toast)
|
// 初始化基础组件(日志、崩溃处理、Toast)
|
||||||
initCoreComponents();
|
initCoreComponents();
|
||||||
// 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
|
// 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
|
||||||
restoreDebugStatus();
|
restoreDebugStatus();
|
||||||
|
// 新增:初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
|
||||||
|
initWinbollHost();
|
||||||
|
|
||||||
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
|
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
|
||||||
}
|
}
|
||||||
@@ -131,6 +175,11 @@ public class GlobalApplication extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增:初始化服务器地址(应用启动时从 SP 读取到内存)
|
||||||
|
private void initWinbollHost() {
|
||||||
|
getWinbollHost(); // 触发从 SP 读取并更新内存
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取应用名称(从 AndroidManifest.xml 的 android:label 读取)
|
* 获取应用名称(从 AndroidManifest.xml 的 android:label 读取)
|
||||||
* @param context 上下文(建议传入 Application 上下文,避免内存泄漏)
|
* @param context 上下文(建议传入 Application 上下文,避免内存泄漏)
|
||||||
@@ -170,7 +219,6 @@ public class GlobalApplication extends Application {
|
|||||||
// 释放单例引用(可选,避免内存泄漏风险)
|
// 释放单例引用(可选,避免内存泄漏风险)
|
||||||
sInstance = null;
|
sInstance = null;
|
||||||
LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
|
LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package cc.winboll.studio.libappbase.dialogs;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.R;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/22 20:59
|
||||||
|
* @Describe WinBoLL服务器地址设置对话框(调试模式专用)
|
||||||
|
*/
|
||||||
|
public class DebugHostDialog extends Dialog implements View.OnClickListener {
|
||||||
|
public static final String TAG = "DebugHostDialog";
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
private EditText etHostInput;
|
||||||
|
private Button btnConfirm;
|
||||||
|
private Button btnCancel;
|
||||||
|
|
||||||
|
// 构造方法(适配默认样式)
|
||||||
|
public DebugHostDialog(Context context) {
|
||||||
|
super(context, R.style.DialogStyle);
|
||||||
|
this.mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.dialog_winboll_host); // 绑定XML布局
|
||||||
|
setCancelable(true); // 点击外部可关闭
|
||||||
|
initView();
|
||||||
|
initData();
|
||||||
|
LogUtils.d(TAG, "DebugHostDialog 初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化视图
|
||||||
|
private void initView() {
|
||||||
|
etHostInput = findViewById(R.id.et_host_input);
|
||||||
|
btnConfirm = findViewById(R.id.btn_confirm);
|
||||||
|
btnCancel = findViewById(R.id.btn_cancel);
|
||||||
|
|
||||||
|
// 绑定点击事件
|
||||||
|
btnConfirm.setOnClickListener(this);
|
||||||
|
btnCancel.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据(显示当前已保存的地址)
|
||||||
|
private void initData() {
|
||||||
|
String currentHost = GlobalApplication.getWinbollHost();
|
||||||
|
if (!TextUtils.isEmpty(currentHost)) {
|
||||||
|
etHostInput.setText(currentHost);
|
||||||
|
etHostInput.setSelection(currentHost.length()); // 光标定位到末尾
|
||||||
|
LogUtils.d(TAG, "当前已保存的服务器地址:" + currentHost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
int id = v.getId();
|
||||||
|
if (id == R.id.btn_confirm) {
|
||||||
|
handleConfirm(); // 确认设置
|
||||||
|
} else if (id == R.id.btn_cancel) {
|
||||||
|
dismiss(); // 取消对话框
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理确认设置逻辑
|
||||||
|
private void handleConfirm() {
|
||||||
|
String inputHost = etHostInput.getText().toString().trim();
|
||||||
|
if (TextUtils.isEmpty(inputHost)) {
|
||||||
|
ToastUtils.show("服务器地址不能为空");
|
||||||
|
LogUtils.w(TAG, "设置失败:地址为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单校验URL格式(避免明显错误)
|
||||||
|
if (!inputHost.startsWith("http://") && !inputHost.startsWith("https://")) {
|
||||||
|
ToastUtils.show("地址需以http://或https://开头");
|
||||||
|
LogUtils.w(TAG, "设置失败:地址格式错误,input=" + inputHost);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存地址到SP+内存
|
||||||
|
GlobalApplication.setWinbollHost(inputHost);
|
||||||
|
ToastUtils.show("服务器地址设置成功");
|
||||||
|
LogUtils.d(TAG, "服务器地址设置成功:" + inputHost);
|
||||||
|
dismiss(); // 关闭对话框
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ import cc.winboll.studio.libappbase.utils.SignGetUtils;
|
|||||||
* @LastEditTime 2026/01/21 11:00:00
|
* @LastEditTime 2026/01/21 11:00:00
|
||||||
*/
|
*/
|
||||||
public class SignGetDialog extends Dialog {
|
public class SignGetDialog extends Dialog {
|
||||||
private static final String TAG = "SignGetDialog";
|
public static final String TAG = "SignGetDialog";
|
||||||
private EditText etSignFingerprint;
|
private EditText etSignFingerprint;
|
||||||
private TextView tvAuthResult;
|
private TextView tvAuthResult;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
@@ -59,19 +59,32 @@ public class SignGetDialog extends Dialog {
|
|||||||
LogUtils.d(TAG, "当前应用签名:" + sign);
|
LogUtils.d(TAG, "当前应用签名:" + sign);
|
||||||
|
|
||||||
// 2. 正版校验+显示结果
|
// 2. 正版校验+显示结果
|
||||||
APPUtils.checkAppValid(mContext);
|
APPUtils.checkAppValid(mContext, new APPUtils.CheckResultCallback() {
|
||||||
boolean isOfficial = isSignValid();
|
@Override
|
||||||
|
public void onResult(boolean isValid, String message) {
|
||||||
String szOfficialMessage;
|
String szOfficialMessage;
|
||||||
if (isOfficial) {
|
// if (isValid) {
|
||||||
|
// // 校验通过,执行正常逻辑
|
||||||
|
// } else {
|
||||||
|
// // 校验失败,提示用户
|
||||||
|
// ToastUtils.show(message);
|
||||||
|
// }
|
||||||
|
if (isValid) {
|
||||||
|
LogUtils.d(TAG, "校验通过:" + message);
|
||||||
szOfficialMessage = "< 这是正版的 WinBoLL 应用,请放心使用。 >";
|
szOfficialMessage = "< 这是正版的 WinBoLL 应用,请放心使用。 >";
|
||||||
tvAuthResult.setTextColor(Color.BLUE);
|
tvAuthResult.setTextColor(Color.BLUE);
|
||||||
} else {
|
} else {
|
||||||
|
LogUtils.e(TAG, "校验失败:" + message);
|
||||||
szOfficialMessage = "< 您使用的可能不是正版的 WinBoLL 应用。 >";
|
szOfficialMessage = "< 您使用的可能不是正版的 WinBoLL 应用。 >";
|
||||||
tvAuthResult.setTextColor(Color.RED);
|
tvAuthResult.setTextColor(Color.RED);
|
||||||
}
|
}
|
||||||
ToastUtils.show(szOfficialMessage);
|
ToastUtils.show(szOfficialMessage);
|
||||||
tvAuthResult.setText(szOfficialMessage);
|
tvAuthResult.setText(szOfficialMessage);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// 核心修改:签名字符串转0/1 bit数组(每2个bit加空格,每16位换行,下一行无前置空格)
|
// 核心修改:签名字符串转0/1 bit数组(每2个bit加空格,每16位换行,下一行无前置空格)
|
||||||
private String convertSignToBitArrayWithWrap(String signStr) {
|
private String convertSignToBitArrayWithWrap(String signStr) {
|
||||||
@@ -120,10 +133,10 @@ public class SignGetDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 校验签名是否合法(匹配APPUtils目标签名)
|
// 校验签名是否合法(匹配APPUtils目标签名)
|
||||||
private boolean isSignValid() {
|
// private boolean isSignValid() {
|
||||||
String currentSign = getCurrentSign();
|
// String currentSign = getCurrentSign();
|
||||||
String targetSign = APPUtils.TARGET_SIGN_FINGERPRINT; // 取APPUtils目标签名
|
// String targetSign = APPUtils.TARGET_SIGN_FINGERPRINT; // 取APPUtils目标签名
|
||||||
return currentSign != null && targetSign != null && currentSign.equals(targetSign);
|
// return currentSign != null && targetSign != null && currentSign.equals(targetSign);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package cc.winboll.studio.libappbase.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/22 20:37
|
||||||
|
*/
|
||||||
|
// ==================== JSON响应模型(与后端返回字段完全匹配)====================
|
||||||
|
public class SignCheckResponse {
|
||||||
|
private int code; // 根节点code(后端返回)
|
||||||
|
private String msg; // 根节点提示信息(后端返回,替换原message)
|
||||||
|
private DataBean data; // 根节点data对象(后端返回)
|
||||||
|
|
||||||
|
// 内部DataBean:对应后端返回的data字段内容
|
||||||
|
public static class DataBean {
|
||||||
|
private boolean valid; // 实际是否合法的标识(后端data.valid)
|
||||||
|
private String signature; // 加密后的签名
|
||||||
|
private String decryptedSign;// 解密后的原始签名
|
||||||
|
private long validTime; // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter/Setter(关键:获取data中的valid字段)
|
||||||
|
public boolean isValid() {
|
||||||
|
return data != null && data.valid; // 从data中获取valid值
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return msg; // 对应后端根节点的msg字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他必要的Getter/Setter(用于后续扩展)
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataBean getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,52 +5,136 @@ import android.content.pm.PackageInfo;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.Signature;
|
import android.content.pm.Signature;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.models.SignCheckResponse;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import java.io.IOException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Date;
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
* @Date 2026/01/20 19:17
|
* @Date 2026/01/20 19:17
|
||||||
* @Describe APPUtils 应用包名、签名校验工具类
|
* @Describe APPUtils 应用包名、签名校验工具类(OKHTTP网络校验版)
|
||||||
*/
|
*/
|
||||||
public class APPUtils {
|
public class APPUtils {
|
||||||
|
|
||||||
public static final String TAG = "APPUtils";
|
public static final String TAG = "APPUtils";
|
||||||
// 目标应用签名指纹(BASE64格式,自行替换为你的合法指纹)
|
// 网络校验接口地址
|
||||||
//public static final String TARGET_SIGN_FINGERPRINT = "你的应用签名SHA1指纹BASE64值";
|
private static final String CHECK_API_URI = "api/app-signatures-check";
|
||||||
public static final String TARGET_SIGN_FINGERPRINT = "bMArVdXE4ZZo42vS9e/kXE63MkE=";
|
// OKHTTP客户端(单例复用)
|
||||||
// 目标应用包名(自行替换为你的应用包名)
|
private static OkHttpClient sOkHttpClient = new OkHttpClient();
|
||||||
private static final String TARGET_PACKAGE_NAME = "cc.winboll.studio.你的应用包名";
|
// Gson解析实例
|
||||||
|
private static Gson sGson = new Gson();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查应用包名+签名指纹合法性,不匹配打日志,匹配无操作
|
* 检查应用合法性(包名校验+OKHTTP网络校验签名)
|
||||||
* @param context 上下文
|
* @param context 上下文
|
||||||
|
* @param callback 校验结果回调(主线程回调)
|
||||||
*/
|
*/
|
||||||
public static void checkAppValid(Context context) {
|
public static void checkAppValid(Context context, final CheckResultCallback callback) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
LogUtils.w(TAG, "checkAppValid: context为空,跳过校验");
|
LogUtils.w(TAG, "checkAppValid: context为空,跳过校验");
|
||||||
|
if (callback != null) callback.onResult(false, "context为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 1. 校验包名
|
|
||||||
String currentPkg = context.getPackageName();
|
// 2. 获取当前应用签名(SHA1+Base64)和证书生效时间
|
||||||
LogUtils.d(TAG, "checkAppValid: 当前应用包名=" + currentPkg + ",目标包名=" + TARGET_PACKAGE_NAME);
|
|
||||||
if (!TARGET_PACKAGE_NAME.equals(currentPkg)) {
|
|
||||||
LogUtils.e(TAG, "checkAppValid: 应用包名不匹配,非法环境");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 2. 校验签名指纹
|
|
||||||
String currentSign = getAppSignFingerprint(context);
|
String currentSign = getAppSignFingerprint(context);
|
||||||
LogUtils.d(TAG, "checkAppValid: 当前应用签名指纹=" + currentSign + ",目标指纹=" + TARGET_SIGN_FINGERPRINT);
|
long certValidTime = getCertValidTime(context); // 证书生效时间(毫秒时间戳)
|
||||||
if (currentSign == null || !TARGET_SIGN_FINGERPRINT.equals(currentSign)) {
|
if (currentSign == null) {
|
||||||
LogUtils.e(TAG, "checkAppValid: 应用签名指纹不匹配,非法环境");
|
String errorMsg = "获取应用签名失败";
|
||||||
|
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
|
||||||
|
if (callback != null) callback.onResult(false, errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:对currentSign进行Base64二次加密(URL安全编码,避免特殊字符)
|
||||||
|
String encryptedSign = base64Encode(currentSign);
|
||||||
|
LogUtils.d(TAG, "checkAppValid: 原始签名=" + currentSign + ",Base64二次加密后=" + encryptedSign);
|
||||||
|
|
||||||
|
// 3. 构建请求URL(拼接加密后的签名参数)
|
||||||
|
String requestUrl = String.format("%s?signature=%s&validTime=%d",
|
||||||
|
GlobalApplication.getWinbollHost() + CHECK_API_URI,
|
||||||
|
encryptedSign, // 替换为加密后的签名
|
||||||
|
certValidTime);
|
||||||
|
LogUtils.d(TAG, "checkAppValid: 发起网络校验请求,URL=" + requestUrl);
|
||||||
|
|
||||||
|
// 4. OKHTTP发起异步GET请求
|
||||||
|
Request request = new Request.Builder().url(requestUrl).build();
|
||||||
|
sOkHttpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
final String errorMsg = "网络校验请求失败:" + e.getMessage();
|
||||||
|
LogUtils.e(TAG, "checkAppValid: " + errorMsg, e);
|
||||||
|
if (callback != null) {
|
||||||
|
// 切换到主线程回调
|
||||||
|
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
callback.onResult(false, errorMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
String responseJson = response.body().string();
|
||||||
|
LogUtils.d(TAG, "checkAppValid: 网络校验响应JSON=" + responseJson);
|
||||||
|
// 解析JSON响应
|
||||||
|
SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
|
||||||
|
final boolean isValid = checkResponse != null && checkResponse.isValid();
|
||||||
|
final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败";
|
||||||
|
if (callback != null) {
|
||||||
|
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
callback.onResult(isValid, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final String errorMsg = "网络校验响应失败,code=" + response.code();
|
||||||
|
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
|
||||||
|
if (callback != null) {
|
||||||
|
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
callback.onResult(false, errorMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增:Base64加密工具(URL安全编码,避免特殊字符影响URL拼接)
|
||||||
|
* @param content 待加密内容
|
||||||
|
* @return 加密后的Base64字符串
|
||||||
|
*/
|
||||||
|
private static String base64Encode(String content) {
|
||||||
|
try {
|
||||||
|
// 使用URL安全的Base64编码(替换+为-,/为_,去除=)
|
||||||
|
byte[] contentBytes = content.getBytes("UTF-8");
|
||||||
|
return Base64.encodeToString(contentBytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "base64Encode: 加密失败", e);
|
||||||
|
return content; // 加密失败则返回原始内容,避免请求异常
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前应用签名SHA1指纹(BASE64编码,适配Java7)
|
* 获取当前应用签名SHA1指纹(BASE64编码)
|
||||||
* @param context 上下文
|
|
||||||
* @return 签名指纹字符串,失败返回null
|
|
||||||
*/
|
*/
|
||||||
private static String getAppSignFingerprint(Context context) {
|
private static String getAppSignFingerprint(Context context) {
|
||||||
try {
|
try {
|
||||||
@@ -61,18 +145,49 @@ public class APPUtils {
|
|||||||
LogUtils.w(TAG, "getAppSignFingerprint: 未获取到应用签名");
|
LogUtils.w(TAG, "getAppSignFingerprint: 未获取到应用签名");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// SHA1摘要 + BASE64编码,和目标指纹格式统一
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||||
md.update(signatures[0].toByteArray());
|
md.update(signatures[0].toByteArray());
|
||||||
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
|
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
|
||||||
LogUtils.e(TAG, "getAppSignFingerprint: 包名未找到", e);
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
LogUtils.e(TAG, "getAppSignFingerprint: 不支持SHA1算法", e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "getAppSignFingerprint: 获取签名异常", e);
|
LogUtils.e(TAG, "getAppSignFingerprint: 获取签名异常", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用证书生效时间(毫秒时间戳)
|
||||||
|
*/
|
||||||
|
private static long getCertValidTime(Context context) {
|
||||||
|
try {
|
||||||
|
PackageManager pm = context.getPackageManager();
|
||||||
|
PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||||
|
Signature[] signatures = pkgInfo.signatures;
|
||||||
|
if (signatures == null || signatures.length == 0) {
|
||||||
|
LogUtils.w(TAG, "getCertValidTime: 未获取到应用签名");
|
||||||
|
return new Date().getTime(); // 默认当前时间
|
||||||
|
}
|
||||||
|
// 解析签名证书,获取生效时间(简化实现,实际需解析X.509证书)
|
||||||
|
// 注意:若需精准获取证书生效时间,需解析Signature的toByteArray()为X509Certificate
|
||||||
|
// 此处为简化版,若需精准实现可告知,将补充完整证书解析逻辑
|
||||||
|
return new Date().getTime();
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
LogUtils.e(TAG, "getCertValidTime: 获取包信息异常", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "getCertValidTime: 未知异常", e);
|
||||||
|
}
|
||||||
|
return new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 校验结果回调接口 ====================
|
||||||
|
public interface CheckResultCallback {
|
||||||
|
/**
|
||||||
|
* 校验结果回调(主线程调用)
|
||||||
|
* @param isValid 是否合法
|
||||||
|
* @param message 校验信息
|
||||||
|
*/
|
||||||
|
void onResult(boolean isValid, String message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import cc.winboll.studio.libappbase.GlobalApplication;
|
|||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.libappbase.R;
|
import cc.winboll.studio.libappbase.R;
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import cc.winboll.studio.libappbase.dialogs.DebugHostDialog;
|
||||||
import cc.winboll.studio.libappbase.dialogs.SignGetDialog;
|
import cc.winboll.studio.libappbase.dialogs.SignGetDialog;
|
||||||
import cc.winboll.studio.libappbase.models.APPInfo;
|
import cc.winboll.studio.libappbase.models.APPInfo;
|
||||||
|
|
||||||
@@ -116,7 +117,8 @@ public class AboutView extends LinearLayout {
|
|||||||
// LogUtils.d(TAG, "initViewFromXml 布局加载+视图绑定完成");
|
// LogUtils.d(TAG, "initViewFromXml 布局加载+视图绑定完成");
|
||||||
// }
|
// }
|
||||||
// 1. 新增视图绑定属性(加在原有视图属性后面)
|
// 1. 新增视图绑定属性(加在原有视图属性后面)
|
||||||
private ImageButton ibSigngetdialog;
|
private ImageButton ibSigngetDialog;
|
||||||
|
private ImageButton ibWinBoLLHostDialog;
|
||||||
|
|
||||||
// 2. 完善initViewFromXml方法,新增按钮绑定
|
// 2. 完善initViewFromXml方法,新增按钮绑定
|
||||||
private void initViewFromXml() {
|
private void initViewFromXml() {
|
||||||
@@ -125,20 +127,29 @@ public class AboutView extends LinearLayout {
|
|||||||
tvAppNameVersion = findViewById(R.id.tv_app_name_version);
|
tvAppNameVersion = findViewById(R.id.tv_app_name_version);
|
||||||
tvAppDesc = findViewById(R.id.tv_app_desc);
|
tvAppDesc = findViewById(R.id.tv_app_desc);
|
||||||
llFunctionContainer = findViewById(R.id.ll_function_container);
|
llFunctionContainer = findViewById(R.id.ll_function_container);
|
||||||
ibSigngetdialog = findViewById(R.id.ib_signgetdialog); // 新增按钮绑定
|
ibSigngetDialog = findViewById(R.id.ib_signgetdialog); // 新增按钮绑定
|
||||||
|
ibWinBoLLHostDialog = findViewById(R.id.ib_winbollhostdialog); // 新增按钮绑定
|
||||||
|
ibWinBoLLHostDialog.setVisibility(GlobalApplication.isDebugging()?View.VISIBLE:View.GONE);
|
||||||
setBtnClickListener(); // 新增绑定点击事件
|
setBtnClickListener(); // 新增绑定点击事件
|
||||||
LogUtils.d(TAG, "initViewFromXml 布局加载+视图绑定完成");
|
LogUtils.d(TAG, "initViewFromXml 布局加载+视图绑定完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 新增按钮点击事件方法(放在initViewFromXml下面即可)
|
// 3. 新增按钮点击事件方法(放在initViewFromXml下面即可)
|
||||||
private void setBtnClickListener() {
|
private void setBtnClickListener() {
|
||||||
ibSigngetdialog.setOnClickListener(new OnClickListener() {
|
ibSigngetDialog.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
LogUtils.d(TAG, "签名获取按钮点击,弹出SignGetDialog");
|
LogUtils.d(TAG, "签名获取按钮点击,弹出SignGetDialog");
|
||||||
new SignGetDialog(mContext).show(); // 弹出对话框
|
new SignGetDialog(mContext).show(); // 弹出对话框
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "签名获取按钮点击,弹出SignGetDialog");
|
||||||
|
new DebugHostDialog(mContext).show(); // 弹出对话框
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
11
libappbase/src/main/res/drawable/ic_bug.xml
Normal file
11
libappbase/src/main/res/drawable/ic_bug.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ff000000"
|
||||||
|
android:pathData="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
60
libappbase/src/main/res/layout/dialog_winboll_host.xml
Normal file
60
libappbase/src/main/res/layout/dialog_winboll_host.xml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="#FFFFFF">
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="设置服务器地址"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="#212121"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<!-- 地址输入框 -->
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_host_input"
|
||||||
|
android:layout_width="300dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="请输入服务器地址(如http://localhost:8080)"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:background="@android:drawable/edit_text"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<!-- 按钮容器 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<!-- 取消按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_cancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="取消"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginRight="8dp"/>
|
||||||
|
|
||||||
|
<!-- 确认按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_confirm"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="确认"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:backgroundTint="#2196F3"
|
||||||
|
android:textColor="#FFFFFF"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -53,17 +53,31 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:spacing="20dp">
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="48dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
|
android:src="@drawable/ic_winboll"
|
||||||
|
android:id="@+id/ib_winbollhostdialog"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:background="@null"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
android:src="@drawable/ic_key"
|
android:src="@drawable/ic_key"
|
||||||
android:id="@+id/ib_signgetdialog"/>
|
android:id="@+id/ib_signgetdialog"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:background="@null"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
|||||||
35
libappbase/src/main/res/xml/network_security_config.xml
Normal file
35
libappbase/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<!-- 原有配置 保留 -->
|
||||||
|
<domain includeSubdomains="true">winboll.cc</domain>
|
||||||
|
<domain includeSubdomains="true">localhost</domain>
|
||||||
|
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||||
|
|
||||||
|
<!-- 精准配置10.8.0.0/24 前20个IP(10.8.0.0~10.8.0.19)-->
|
||||||
|
<domain includeSubdomains="false">10.8.0.0</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.1</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.2</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.3</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.4</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.5</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.6</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.7</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.8</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.9</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.10</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.11</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.12</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.13</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.14</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.15</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.16</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.17</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.18</domain>
|
||||||
|
<domain includeSubdomains="false">10.8.0.19</domain>
|
||||||
|
</domain-config>
|
||||||
|
</network-security-config>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user