Compare commits

...

12 Commits

18 changed files with 954 additions and 82 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jan 13 03:23:34 HKT 2026
stageCount=5
#Fri Jan 23 03:11:07 HKT 2026
stageCount=8
libraryProject=libappbase
baseVersion=15.15
publishVersion=15.15.4
publishVersion=15.15.7
buildCount=0
baseBetaVersion=15.15.5
baseBetaVersion=15.15.8

View File

@@ -21,5 +21,11 @@ android {
}
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'])
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jan 13 03:23:17 HKT 2026
stageCount=5
#Fri Jan 23 03:11:07 HKT 2026
stageCount=8
libraryProject=libappbase
baseVersion=15.15
publishVersion=15.15.4
publishVersion=15.15.7
buildCount=0
baseBetaVersion=15.15.5
baseBetaVersion=15.15.8

View File

@@ -3,7 +3,11 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libappbase">
<application>
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".CrashHandler$CrashActivity"

View File

@@ -2,6 +2,7 @@ package cc.winboll.studio.libappbase;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -26,6 +27,12 @@ public class GlobalApplication extends Application {
*/
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 单例实例(外部可通过此方法获取上下文)
* @return GlobalApplication 单例(未初始化时返回 null需确保配置 AndroidManifest
@@ -53,7 +60,7 @@ public class GlobalApplication extends Application {
}
// 将调试状态封装为 APPModel 并保存到文件
APPModel.saveBeanToFile(
getAppModelFilePath(application),
getAppModelFilePath(application),
new APPModel(isDebugging)
);
}
@@ -75,6 +82,42 @@ public class GlobalApplication extends Application {
public static boolean 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;
}
/**
* 应用启动时初始化(仅执行一次)
@@ -85,12 +128,13 @@ public class GlobalApplication extends Application {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
// 初始化基础组件日志、崩溃处理、Toast
initCoreComponents();
// 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
restoreDebugStatus();
// 新增:初始化服务器地址(从 SP 读取到内存,提高后续访问效率)
initWinbollHost();
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
}
@@ -115,7 +159,7 @@ public class GlobalApplication extends Application {
private void restoreDebugStatus() {
// 从文件加载 APPModel 实例(存储调试状态的模型类)
APPModel appModel = APPModel.loadBeanFromFile(
getAppModelFilePath(this),
getAppModelFilePath(this),
APPModel.class
);
@@ -131,6 +175,11 @@ public class GlobalApplication extends Application {
}
}
// 新增:初始化服务器地址(应用启动时从 SP 读取到内存)
private void initWinbollHost() {
getWinbollHost(); // 触发从 SP 读取并更新内存
}
/**
* 获取应用名称(从 AndroidManifest.xml 的 android:label 读取)
* @param context 上下文(建议传入 Application 上下文,避免内存泄漏)
@@ -154,7 +203,7 @@ public class GlobalApplication extends Application {
return appName;
} catch (NameNotFoundException e) {
// 包名不存在(理论上不会发生,捕获异常避免崩溃)
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
e.printStackTrace();
}
@@ -170,7 +219,6 @@ public class GlobalApplication extends Application {
// 释放单例引用(可选,避免内存泄漏风险)
sInstance = null;
LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
}
}

View File

@@ -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(); // 关闭对话框
}
}

View File

@@ -0,0 +1,142 @@
package cc.winboll.studio.libappbase.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.APPUtils;
import cc.winboll.studio.libappbase.utils.SignGetUtils;
/**
* @Describe 签名显示+正版校验对话框
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/20 21:20:00
* @LastEditTime 2026/01/21 11:00:00
*/
public class SignGetDialog extends Dialog {
public static final String TAG = "SignGetDialog";
private EditText etSignFingerprint;
private TextView tvAuthResult;
private Context mContext;
public SignGetDialog(Context context) {
super(context, R.style.DialogStyle); // 适配默认对话框样式
this.mContext = context;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_sign_get); // 绑定xml布局
setCancelable(true); // 点击外部可关闭
initView();
initSignAndCheck(); // 获取签名+正版校验
}
private void initView() {
etSignFingerprint = findViewById(R.id.et_sign_fingerprint);
tvAuthResult = findViewById(R.id.tv_auth_result);
// 输入框只读,方便复制
etSignFingerprint.setEnabled(false);
}
// 核心:获取签名+调用APPUtils校验
private void initSignAndCheck() {
// 1. 获取当前应用签名
String sign = getCurrentSign();
if (sign == null) {
etSignFingerprint.setText("签名获取失败");
} else {
// 签名字符串转0/1 bit数组每2个bit加空格每16位换行下一行无前置空格
String bitArrayStr = convertSignToBitArrayWithWrap(sign);
etSignFingerprint.setText(bitArrayStr);
}
LogUtils.d(TAG, "当前应用签名:" + sign);
// 2. 正版校验+显示结果
APPUtils.checkAppValid(mContext, new APPUtils.CheckResultCallback() {
@Override
public void onResult(boolean isValid, String message) {
String szOfficialMessage;
// if (isValid) {
// // 校验通过,执行正常逻辑
// } else {
// // 校验失败,提示用户
// ToastUtils.show(message);
// }
if (isValid) {
LogUtils.d(TAG, "校验通过:" + message);
szOfficialMessage = "< 这是正版的 WinBoLL 应用,请放心使用。 >";
tvAuthResult.setTextColor(Color.BLUE);
} else {
LogUtils.e(TAG, "校验失败:" + message);
szOfficialMessage = "< 您使用的可能不是正版的 WinBoLL 应用。 >";
tvAuthResult.setTextColor(Color.RED);
}
ToastUtils.show(szOfficialMessage);
tvAuthResult.setText(szOfficialMessage);
}
});
}
// 核心修改签名字符串转0/1 bit数组每2个bit加空格每16位换行下一行无前置空格
private String convertSignToBitArrayWithWrap(String signStr) {
StringBuilder bitBuilder = new StringBuilder();
// 1. 字符转8位bit
for (char c : signStr.toCharArray()) {
String bit8 = String.format("%8s", Integer.toBinaryString(c)).replace(' ', '0');
bitBuilder.append(bit8);
}
String fullBitStr = bitBuilder.toString();
// 2. 按16位分组组内每2个bit加空格避免换行后带空格
StringBuilder finalBuilder = new StringBuilder();
int groupSize = 16; // 每组16个bit
for (int i = 0; i < fullBitStr.length(); i += groupSize) {
// 截取16位bit为一组
int end = Math.min(i + groupSize, fullBitStr.length());
String group = fullBitStr.substring(i, end);
// 组内每2个bit加空格
StringBuilder groupWithSpace = new StringBuilder();
for (int j = 0; j < group.length(); j++) {
groupWithSpace.append(group.charAt(j));
if ((j + 1) % 2 == 0 && j != group.length() - 1) {
groupWithSpace.append(" ");
}
}
// 添加组到最终结果,每组后换行(最后一组不换行)
finalBuilder.append(groupWithSpace);
if (end < fullBitStr.length()) {
finalBuilder.append("\n");
}
}
return finalBuilder.toString();
}
// 获取签名复用SignGetUtils逻辑避免重复代码
private String getCurrentSign() {
try {
return SignGetUtils.getSignStr(mContext); // 复用工具类逻辑
} catch (Exception e) {
LogUtils.e(TAG, "获取签名失败", e);
return null;
}
}
// 校验签名是否合法匹配APPUtils目标签名
// private boolean isSignValid() {
// String currentSign = getCurrentSign();
// String targetSign = APPUtils.TARGET_SIGN_FINGERPRINT; // 取APPUtils目标签名
// return currentSign != null && targetSign != null && currentSign.equals(targetSign);
// }
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,193 @@
package cc.winboll.studio.libappbase.utils;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64;
import cc.winboll.studio.libappbase.GlobalApplication;
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.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>
* @Date 2026/01/20 19:17
* @Describe APPUtils 应用包名、签名校验工具类OKHTTP网络校验版
*/
public class APPUtils {
public static final String TAG = "APPUtils";
// 网络校验接口地址
private static final String CHECK_API_URI = "api/app-signatures-check";
// OKHTTP客户端单例复用
private static OkHttpClient sOkHttpClient = new OkHttpClient();
// Gson解析实例
private static Gson sGson = new Gson();
/**
* 检查应用合法性(包名校验+OKHTTP网络校验签名
* @param context 上下文
* @param callback 校验结果回调(主线程回调)
*/
public static void checkAppValid(Context context, final CheckResultCallback callback) {
if (context == null) {
LogUtils.w(TAG, "checkAppValid: context为空跳过校验");
if (callback != null) callback.onResult(false, "context为空");
return;
}
// 2. 获取当前应用签名SHA1+Base64和证书生效时间
String currentSign = getAppSignFingerprint(context);
long certValidTime = getCertValidTime(context); // 证书生效时间(毫秒时间戳)
if (currentSign == null) {
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编码
*/
private static String getAppSignFingerprint(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, "getAppSignFingerprint: 未获取到应用签名");
return null;
}
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(signatures[0].toByteArray());
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getAppSignFingerprint: 获取签名异常", e);
} catch (Exception e) {
LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e);
}
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);
}
}

View File

@@ -0,0 +1,71 @@
package cc.winboll.studio.libappbase.utils;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64;
import cc.winboll.studio.libappbase.LogUtils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/20 19:50
* @Describe 获取应用签名指纹SHA1+Base64直接复制用
*/
public class SignGetUtils {
private static final String TAG = "SignGetUtils";
/**
* 一键获取当前应用签名指纹(直接调用,看日志复制结果)
*/
public static void getCurrentAppSign(Context context) {
if (context == null) {
LogUtils.e(TAG, "context不能为空");
return;
}
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.e(TAG, "未获取到应用签名");
return;
}
// 和APPUtils校验格式完全一致SHA1+Base64 NO_WRAP
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(signatures[0].toByteArray());
String signBase64 = Base64.encodeToString(md.digest(), Base64.NO_WRAP);
// 关键日志复制【】里的内容到APPUtils的TARGET_SIGN_FINGERPRINT
LogUtils.d(TAG, "当前应用包名:" + context.getPackageName());
LogUtils.d(TAG, "当前应用签名指纹(直接复制):【" + signBase64 + "");
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "获取签名失败:包名不存在", e);
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "获取签名失败不支持SHA1", e);
} catch (Exception e) {
LogUtils.e(TAG, "获取签名失败", e);
}
}
// 新增:直接返回签名字符串,供对话框调用
public static String getSignStr(Context context) {
if (context == null) return null;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = pkgInfo.signatures;
if (signatures == null || signatures.length == 0) return null;
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(signatures[0].toByteArray());
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
} catch (Exception e) {
LogUtils.e(TAG, "获取签名字符串失败", e);
return null;
}
}
}

View File

@@ -8,21 +8,23 @@ import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R;
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.models.APPInfo;
/**
* @Describe AboutView 原生实现关于页面无第三方依赖适配API30抽象通用功能控件邮件/网页跳转)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/11 12:23:00
* @LastEditTime 2026/01/12 01:05:30
* @LastEditTime 2026/01/20 20:45:00
*/
public class AboutView extends LinearLayout {
// 全局常量区(标识、回调标识)
@@ -41,7 +43,6 @@ public class AboutView extends LinearLayout {
private static final int PADDING_MID = 16;
private static final int PADDING_SMALL = 8;
private static final int ICON_SIZE = 48;
private static final int LINE_HEIGHT = 1;
private static final int ITEM_ICON_SIZE = 24;
// 成员属性区(按 核心依赖→业务配置→视图相关 归类排序,注释清晰)
@@ -66,12 +67,19 @@ public class AboutView extends LinearLayout {
private EditText metDevUserName; // 调试用户名输入框
private EditText metDevUserPassword; // 调试密码输入框
// 视图绑定
private ImageView ivAppIcon;
private TextView tvAppNameVersion;
private TextView tvAppDesc;
private LinearLayout llFunctionContainer;
// 构造方法区(按 参数从少到多 排序,适配 代码创建+XML引用 场景)
public AboutView(Context context) {
super(context);
LogUtils.d(TAG, "AboutView(Context) 构造方法调用,代码创建视图场景");
this.mContext = context;
initDefaultParams();
initViewFromXml();
}
public AboutView(Context context, APPInfo appInfo) {
@@ -79,6 +87,7 @@ public class AboutView extends LinearLayout {
LogUtils.d(TAG, "AboutView(Context,APPInfo) 构造调用入参APPInfo" + (appInfo == null ? "null" : appInfo.getAppName()));
this.mContext = context;
this.mAPPInfo = appInfo;
initViewFromXml();
initAll();
}
@@ -87,6 +96,7 @@ public class AboutView extends LinearLayout {
LogUtils.d(TAG, "AboutView(Context,AttributeSet) 构造调用XML布局引用场景");
this.mContext = context;
initDefaultParams();
initViewFromXml();
}
public AboutView(Context context, AttributeSet attrs, int defStyleAttr) {
@@ -94,8 +104,55 @@ public class AboutView extends LinearLayout {
LogUtils.d(TAG, "AboutView(Context,AttributeSet,int) 构造调用XML布局+样式配置defStyleAttr" + defStyleAttr);
this.mContext = context;
initDefaultParams();
initViewFromXml();
}
// 核心加载xml布局并绑定视图
// private void initViewFromXml() {
// View.inflate(mContext, R.layout.layout_about_view, this);
// ivAppIcon = findViewById(R.id.iv_app_icon);
// tvAppNameVersion = findViewById(R.id.tv_app_name_version);
// tvAppDesc = findViewById(R.id.tv_app_desc);
// llFunctionContainer = findViewById(R.id.ll_function_container);
// LogUtils.d(TAG, "initViewFromXml 布局加载+视图绑定完成");
// }
// 1. 新增视图绑定属性(加在原有视图属性后面)
private ImageButton ibSigngetDialog;
private ImageButton ibWinBoLLHostDialog;
// 2. 完善initViewFromXml方法新增按钮绑定
private void initViewFromXml() {
View.inflate(mContext, R.layout.layout_about_view, this);
ivAppIcon = findViewById(R.id.iv_app_icon);
tvAppNameVersion = findViewById(R.id.tv_app_name_version);
tvAppDesc = findViewById(R.id.tv_app_desc);
llFunctionContainer = findViewById(R.id.ll_function_container);
ibSigngetDialog = findViewById(R.id.ib_signgetdialog); // 新增按钮绑定
ibWinBoLLHostDialog = findViewById(R.id.ib_winbollhostdialog); // 新增按钮绑定
ibWinBoLLHostDialog.setVisibility(GlobalApplication.isDebugging()?View.VISIBLE:View.GONE);
setBtnClickListener(); // 新增绑定点击事件
LogUtils.d(TAG, "initViewFromXml 布局加载+视图绑定完成");
}
// 3. 新增按钮点击事件方法放在initViewFromXml下面即可
private void setBtnClickListener() {
ibSigngetDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "签名获取按钮点击弹出SignGetDialog");
new SignGetDialog(mContext).show(); // 弹出对话框
}
});
ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "签名获取按钮点击弹出SignGetDialog");
new DebugHostDialog(mContext).show(); // 弹出对话框
}
});
}
// 对外公开方法区(供外部调用,职责单一,注释明确)
/**
* 一站式初始化所有关于页逻辑,包含参数、信息、视图全流程初始化
@@ -106,10 +163,6 @@ public class AboutView extends LinearLayout {
LogUtils.w(TAG, "initAll() 初始化终止APPInfo 为 null无法获取应用核心信息");
return;
}
// 基础布局配置
setOrientation(VERTICAL);
setPadding(dp2px(PADDING_MID), dp2px(PADDING_LARGE), dp2px(PADDING_MID), dp2px(PADDING_LARGE));
setGravity(Gravity.CENTER_HORIZONTAL);
// 按初始化流程执行,有序无冗余
initDefaultParams();
@@ -129,7 +182,7 @@ public class AboutView extends LinearLayout {
public void setAPPInfoAndInit(APPInfo appInfo) {
LogUtils.d(TAG, "setAPPInfoAndInit() 调用传入新APPInfo" + (appInfo == null ? "null" : appInfo.getAppName()));
this.mAPPInfo = appInfo;
removeAllViews();
llFunctionContainer.removeAllViews();
initAll();
LogUtils.d(TAG, "setAPPInfoAndInit() 应用信息重置+页面重构完成");
}
@@ -141,7 +194,7 @@ public class AboutView extends LinearLayout {
public void setAPPInfo(APPInfo appInfo) {
LogUtils.d(TAG, "setAPPInfo() 调用传入APPInfo" + (appInfo == null ? "null" : appInfo.getAppName()));
this.mAPPInfo = appInfo;
removeAllViews();
llFunctionContainer.removeAllViews();
initAll();
}
@@ -196,7 +249,7 @@ public class AboutView extends LinearLayout {
LogUtils.d(TAG, "initAppVersionInfo() 获取版本号失败默认赋值unknown", e);
mszAppVersionName = "unknown";
}
mszCurrentAppPackageName = String.format("%s_%s.apk", mszAppAPKName, mszAppVersionName);
mszCurrentAppPackageName = String.format("%s_%s.apk", mszAppVersionName, mszAppVersionName);
LogUtils.d(TAG, "initAppVersionInfo() 完成,版本号:" + mszAppVersionName + "当前APK名" + mszCurrentAppPackageName);
}
@@ -245,77 +298,37 @@ public class AboutView extends LinearLayout {
}
/**
* 核心视图组装:按 图标→应用信息→分割线→通用功能控件 顺序构建页面
* 核心视图组装:赋值基础信息+添加功能项
*/
private void initAboutPageView() {
LogUtils.d(TAG, "initAboutPageView() 开始组装关于页视图");
addAppIcon();
addAppInfoDesc();
addLineSeparator();
// 基础信息赋值
ivAppIcon.setImageResource(mnAppIcon);
tvAppNameVersion.setText(String.format("%s %s", mszAppName, mszAppVersionName));
if (mszAppDescription.isEmpty()) {
tvAppDesc.setVisibility(GONE);
} else {
tvAppDesc.setVisibility(VISIBLE);
tvAppDesc.setText(mszAppDescription);
}
// 通用功能控件:网页跳转类+邮件类,复用抽象控件
addView(new WebJumpFunctionItemView(mContext, "WinBoLL 主页", WINBOLL_OFFICIAL_HOME, R.drawable.ic_winboll));
addView(new EmailFunctionItemView(mContext, "联系邮箱", "WinBoLLStudio<studio@winboll.cc>", R.drawable.ic_winboll));
addFunctionView(new WebJumpFunctionItemView(mContext, "WinBoLL 主页", WINBOLL_OFFICIAL_HOME, R.drawable.ic_winboll));
addFunctionView(new EmailFunctionItemView(mContext, "联系邮箱", "WinBoLLStudio<studio@winboll.cc>", R.drawable.ic_winboll));
if (!mszHomePage.isEmpty()) {
addView(new WebJumpFunctionItemView(mContext, "应用APK下载地址", mszHomePage, R.drawable.ic_winboll));
addFunctionView(new WebJumpFunctionItemView(mContext, "应用APK下载地址", mszHomePage, R.drawable.ic_winboll));
}
if (!mszGitea.isEmpty()) {
addView(new WebJumpFunctionItemView(mContext, "应用Git源码地址", mszGitea, R.drawable.ic_winboll));
addFunctionView(new WebJumpFunctionItemView(mContext, "应用Git源码地址", mszGitea, R.drawable.ic_winboll));
}
LogUtils.d(TAG, "initAboutPageView() 视图组装完成,功能项加载完毕");
}
// 视图构建辅助方法区(基础视图组件)
/**
* 添加应用图标组件,居中展示
*/
private void addAppIcon() {
ImageView ivIcon = new ImageView(mContext);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dp2px(ICON_SIZE), dp2px(ICON_SIZE));
params.bottomMargin = dp2px(PADDING_MID);
ivIcon.setLayoutParams(params);
ivIcon.setImageResource(mnAppIcon);
ivIcon.setScaleType(ImageView.ScaleType.CENTER_CROP);
addView(ivIcon);
}
/**
* 添加应用名称+版本号+描述信息组件,垂直居中展示
*/
private void addAppInfoDesc() {
LinearLayout llDesc = new LinearLayout(mContext);
llDesc.setOrientation(VERTICAL);
llDesc.setGravity(Gravity.CENTER);
llDesc.setPadding(0, 0, 0, dp2px(PADDING_MID));
TextView tvAppName = new TextView(mContext);
tvAppName.setText(String.format("%s %s", mszAppName, mszAppVersionName));
tvAppName.setTextSize(18);
tvAppName.setTextColor(mContext.getResources().getColor(R.color.gray_900));
llDesc.addView(tvAppName);
if (!mszAppDescription.isEmpty()) {
TextView tvDesc = new TextView(mContext);
tvDesc.setText(mszAppDescription);
tvDesc.setTextSize(14);
tvDesc.setTextColor(mContext.getResources().getColor(R.color.gray_500));
tvDesc.setPadding(0, dp2px(PADDING_SMALL), 0, 0);
llDesc.addView(tvDesc);
}
addView(llDesc);
}
/**
* 添加视图分割线,区分不同功能模块
*/
private void addLineSeparator() {
View line = new View(mContext);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, dp2px(LINE_HEIGHT));
// 添加功能项到容器
private void addFunctionView(View view) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
params.topMargin = dp2px(PADDING_SMALL);
params.bottomMargin = dp2px(PADDING_MID);
line.setLayoutParams(params);
line.setBackgroundColor(mContext.getResources().getColor(R.color.gray_200));
addView(line);
llFunctionContainer.addView(view, params);
}
// 工具方法区(通用工具+业务工具,静态优先,便于复用)

View 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>

View 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="M7,14C5.9,14 5,13.1 5,12S5.9,10 7,10 9,10.9 9,12 8.1,14 7,14M12.6,10C11.8,7.7 9.6,6 7,6C3.7,6 1,8.7 1,12S3.7,18 7,18C9.6,18 11.8,16.3 12.6,14H16V18H20V14H23V10H12.6Z"/>
</vector>

View File

@@ -0,0 +1,52 @@
<?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:gravity="center"
android:background="#FFDCDCDC">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="应用指纹校验"
android:textSize="16sp"
android:textColor="@color/gray_900"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<EditText
android:id="@+id/et_sign_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:drawable/edit_text"
android:textSize="12sp"
android:gravity="top"
android:hint="签名获取中..."
android:singleLine="false"
android:scrollHorizontally="false"
android:scrollbars="vertical"
android:overScrollMode="always"
android:typeface="monospace"
android:paddingLeft="10dp"
android:paddingRight="10dp"/>
</ScrollView>
<TextView
android:id="@+id/tv_auth_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textSize="11sp"
android:gravity="center"
android:textColor="@color/gray_900"/>
</LinearLayout>

View 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>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingLeft="16dp"
android:paddingTop="32dp"
android:paddingRight="16dp"
android:paddingBottom="32dp">
<ImageView
android:id="@+id/iv_app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/tv_app_name_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/gray_900"/>
<TextView
android:id="@+id/tv_app_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:textSize="14sp"
android:textColor="@color/gray_500"/>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:background="@color/gray_200"/>
<LinearLayout
android:id="@+id/ll_function_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="16dp"
android:spacing="20dp">
<ImageButton
android:layout_width="48dp"
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:id="@+id/ib_signgetdialog"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:background="@null"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -11,5 +11,10 @@
<item name="colorText">#FF00B322</item>
<item name="colorTextBackgound">#FF000000</item>
</style>
<style name="DialogStyle" parent="@android:style/Theme.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

View 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个IP10.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>