Compare commits

..

11 Commits

7 changed files with 648 additions and 618 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sat Jan 24 11:26:28 HKT 2026 #Sat Jan 24 20:32:20 HKT 2026
stageCount=9 stageCount=12
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.15 baseVersion=15.15
publishVersion=15.15.8 publishVersion=15.15.11
buildCount=0 buildCount=0
baseBetaVersion=15.15.9 baseBetaVersion=15.15.12

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sat Jan 24 11:26:28 HKT 2026 #Sat Jan 24 20:32:20 HKT 2026
stageCount=9 stageCount=12
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.15 baseVersion=15.15
publishVersion=15.15.8 publishVersion=15.15.11
buildCount=0 buildCount=0
baseBetaVersion=15.15.9 baseBetaVersion=15.15.12

View File

@@ -0,0 +1,186 @@
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.ApkSignUtils;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026-01-20 21:20:00
* @LastEditTime 2026-01-24 18:45:00
* @Describe 签名显示+正版校验对话框:展示应用签名字节位信息,调用网络接口完成正版合法性校验,实时返回校验结果
*/
public class AppValidationDialog extends Dialog {
// ===================================== 全局常量 =====================================
public static final String TAG = "AppValidationDialog";
// 签名字节位分组大小
private static final int BIT_GROUP_SIZE = 16;
// ===================================== 控件与上下文属性 =====================================
private Context mContext;
private EditText etSignFingerprint;
private TextView tvAuthResult;
// ===================================== 业务入参属性 =====================================
private String projectName;
private String versionName;
private String clientSign;
private String clientHash;
// ===================================== 构造方法 =====================================
public AppValidationDialog(Context context, String projectName, String versionName) {
super(context, R.style.DialogStyle);
this.mContext = context;
this.projectName = projectName;
this.versionName = versionName;
LogUtils.d(TAG, "AppValidationDialog: 构造方法初始化,入参-> projectName=" + projectName + ", versionName=" + versionName);
}
// ===================================== 生命周期方法 =====================================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "onCreate: 对话框创建,开始初始化布局与业务逻辑");
setContentView(R.layout.dialog_sign_get);
setCancelable(true);
// 初始化应用签名与哈希
initSignAndHash();
// 初始化页面控件
initView();
// 执行签名展示与正版校验
doSignShowAndAuthCheck();
LogUtils.d(TAG, "onCreate: 对话框初始化流程执行完成");
}
// ===================================== 页面与数据初始化方法 =====================================
/**
* 初始化页面控件,绑定视图并设置基础属性
*/
private void initView() {
LogUtils.d(TAG, "initView: 开始初始化页面控件");
etSignFingerprint = findViewById(R.id.et_sign_fingerprint);
tvAuthResult = findViewById(R.id.tv_auth_result);
// 签名显示框设为只读,方便用户复制
etSignFingerprint.setEnabled(false);
// 填充签名字节位信息
etSignFingerprint.setText(convertSignToBitArrayWithWrap(clientSign));
LogUtils.d(TAG, "initView: 控件初始化完成,已填充签名字节位信息");
}
/**
* 初始化应用签名与SHA256哈希调用工具类获取与服务端对齐的参数
*/
private void initSignAndHash() {
LogUtils.d(TAG, "initSignAndHash: 开始获取应用签名与SHA256哈希");
this.clientSign = ApkSignUtils.getApkSignAlignedWithServer(mContext);
this.clientHash = ApkSignUtils.getApkSHA256Hash(mContext);
LogUtils.d(TAG, "initSignAndHash: 签名与哈希获取完成-> clientSign=" + clientSign + ", clientHash=" + clientHash);
}
// ===================================== 核心业务方法 =====================================
/**
* 核心业务:展示签名字节位信息,发起网络正版校验请求
*/
private void doSignShowAndAuthCheck() {
LogUtils.d(TAG, "doSignShowAndAuthCheck: 开始执行应用正版合法性校验");
// 校验签名与哈希非空,避免空参请求
if (clientSign == null || clientHash == null) {
String errorMsg = "应用签名或哈希获取失败,无法执行正版校验";
LogUtils.e(TAG, "doSignShowAndAuthCheck: " + errorMsg);
tvAuthResult.setTextColor(Color.RED);
tvAuthResult.setText(errorMsg);
ToastUtils.show(errorMsg);
return;
}
// 调用网络校验接口
new APPUtils().checkAPKValidation(
mContext,
projectName,
versionName,
clientSign,
clientHash,
new APPUtils.CheckResultCallback() {
@Override
public void onResult(boolean isValid, String message) {
LogUtils.d(TAG, "checkAPKValidation: 校验结果返回-> isValid=" + isValid + ", message=" + message);
handleAuthResult(isValid, message);
}
}
);
}
/**
* 处理正版校验结果更新UI并提示用户
* @param isValid 校验是否通过
* @param message 服务端返回提示信息
*/
private void handleAuthResult(boolean isValid, String message) {
String showMessage;
if (isValid) {
showMessage = "< 这是正版的 WinBoLL 应用,请放心使用。 >";
tvAuthResult.setTextColor(Color.BLUE);
LogUtils.d(TAG, "handleAuthResult: 正版校验通过," + showMessage + ",服务端信息:" + message);
} else {
showMessage = "< 您使用的可能不是正版的 WinBoLL 应用。 >";
tvAuthResult.setTextColor(Color.RED);
LogUtils.e(TAG, "handleAuthResult: 正版校验失败," + showMessage + ",失败原因:" + message);
}
// 更新UI并弹提示
tvAuthResult.setText(showMessage);
ToastUtils.show(showMessage);
}
// ===================================== 工具方法 =====================================
/**
* 签名字符串转0/1比特数组格式每2个bit加空格每16位换行提升可读性
* @param signStr 原始签名字符串
* @return 格式化后的比特数字符串,签名字符为空返回空串
*/
private String convertSignToBitArrayWithWrap(String signStr) {
LogUtils.d(TAG, "convertSignToBitArrayWithWrap: 开始格式化签名字符串为比特数组");
if (signStr == null || signStr.isEmpty()) {
LogUtils.w(TAG, "convertSignToBitArrayWithWrap: 原始签名字符串为空,返回空串");
return "";
}
// 字符转8位补零的二进制字符串
StringBuilder bitBuilder = new StringBuilder();
for (char c : signStr.toCharArray()) {
String bit8 = String.format("%8s", Integer.toBinaryString(c)).replace(' ', '0');
bitBuilder.append(bit8);
}
String fullBitStr = bitBuilder.toString();
LogUtils.d(TAG, "convertSignToBitArrayWithWrap: 签名转二进制完成,总长度=" + fullBitStr.length() + "bit");
// 按16位分组组内每2bit加空格分组后换行
StringBuilder finalBuilder = new StringBuilder();
for (int i = 0; i < fullBitStr.length(); i += BIT_GROUP_SIZE) {
int end = Math.min(i + BIT_GROUP_SIZE, fullBitStr.length());
String group = fullBitStr.substring(i, end);
// 组内加空格
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");
}
}
LogUtils.d(TAG, "convertSignToBitArrayWithWrap: 签名比特数组格式化完成");
return finalBuilder.toString();
}
}

View File

@@ -1,141 +0,0 @@
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;
String projectName;
String versionName;
public SignGetDialog(Context context, String projectName, String versionName) {
super(context, R.style.DialogStyle); // 适配默认对话框样式
this.mContext = context;
this.projectName = projectName;
this.versionName = versionName;
}
@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() {
// 2. 正版校验+显示结果
// 调用处直接删除base64SignFingerprint参数即可
new APPUtils().checkAPKValidation(
mContext,
this.projectName,
this.versionName,
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

@@ -1,10 +1,6 @@
package cc.winboll.studio.libappbase.utils; package cc.winboll.studio.libappbase.utils;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Base64; import android.util.Base64;
@@ -15,16 +11,10 @@ import cc.winboll.studio.libappbase.models.SignCheckResponse;
import com.google.gson.Gson; import com.google.gson.Gson;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
@@ -35,280 +25,173 @@ import okhttp3.Response;
/** /**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com> * @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026-01-20 19:17:00 * @CreateTime 2026-01-20 19:17:00
* @LastEditTime 2026-01-24 02:18:00 * @LastEditTime 2026-01-24 17:58:00
* @Describe APPUtils 应用包名、签名校验工具类OKHTTP网络校验版兼容Java7含URL编码+APK包签名+SHA256哈希校验 * @Describe APPUtils 应用合法性校验工具类OKHTTP网络校验版兼容Java7
* 对外传入签名/哈希值,拼接调试标识后发起网络校验,主线程返回校验结果
*/ */
public class APPUtils { public class APPUtils {
// ===================================== 全局常量/属性 ===================================== // ===================================== 全局常量/单例属性 =====================================
public static final String TAG = "APPUtils"; public static final String TAG = "APPUtils";
// 网络校验接口地址 // 网络校验接口基础地址
private static final String CHECK_API_URI = "api/app-signatures-check"; private static final String CHECK_API_URI = "api/app-signatures-check";
// OKHTTP客户端单例复用) // OKHTTP客户端单例复用连接,避免资源浪费
private static OkHttpClient sOkHttpClient = new OkHttpClient(); private static final OkHttpClient sOkHttpClient = new OkHttpClient();
// Gson解析例(单例复用 // Gson解析例(全局复用,提高解析效率
private static Gson sGson = new Gson(); private static final Gson sGson = new Gson();
// ===================================== 对外核心方法 ===================================== // ===================================== 对外核心校验方法 =====================================
/** /**
* 检查应用合法性(签名校验+APK哈希校验+网络接口校验) * 检查应用合法性(外部传入签名+哈希,拼接调试标识发起网络校验)
* @param context 上下文 * @param context 上下文,用于主线程回调
* @param projectName 项目名称 * @param projectName 项目名称(服务端区分项目标识)
* @param versionName 应用版本名 * @param versionName 应用版本名(服务端版本校验)
* @param callback 校验结果回调(主线程回调 * @param clientSign 外部计算的应用签名字符串Base64
* @param clientHash 外部计算的APK SHA256哈希字符串小写16进制
* @param callback 校验结果回调(主线程调用,返回是否合法+提示信息)
*/ */
public void checkAPKValidation(Context context, String projectName, String versionName, final CheckResultCallback callback) { public void checkAPKValidation(Context context, String projectName, String versionName,
// 入参调试日志 String clientSign, String clientHash, final CheckResultCallback callback) {
LogUtils.d(TAG, "checkAPKValidation: 入参 projectName=" + projectName + ", versionName=" + versionName); // 方法调用+全量入参调试日志
// 空参校验 LogUtils.d(TAG, "checkAPKValidation: 方法调用,入参-> projectName=" + projectName
+ ", versionName=" + versionName + ", clientSign=" + clientSign + ", clientHash=" + clientHash);
// 1. 核心入参空值校验(快速失败)
if (context == null) { if (context == null) {
LogUtils.w(TAG, "checkAPKValidation: 入参context为空跳过校验"); LogUtils.w(TAG, "checkAPKValidation: 入参context为空直接返回校验失败");
if (callback != null) { callCallbackOnMainThread(callback, false, "上下文对象不能为空");
callback.onResult(false, "context为空");
}
return; return;
} }
if (projectName == null || projectName.trim().isEmpty()) { if (isStringEmpty(projectName)) {
LogUtils.w(TAG, "checkAPKValidation: 入参projectName为空,跳过校验"); LogUtils.w(TAG, "checkAPKValidation: 入参projectName为空/空白,直接返回校验失败");
if (callback != null) { callCallbackOnMainThread(callback, false, "项目名称不能为空");
callback.onResult(false, "projectName为空");
}
return; return;
} }
if (versionName == null || versionName.trim().isEmpty()) { if (isStringEmpty(versionName)) {
LogUtils.w(TAG, "checkAPKValidation: 入参versionName为空,跳过校验"); LogUtils.w(TAG, "checkAPKValidation: 入参versionName为空/空白,直接返回校验失败");
if (callback != null) { callCallbackOnMainThread(callback, false, "应用版本名不能为空");
callback.onResult(false, "versionName为空");
}
return; return;
} }
if (isStringEmpty(clientSign)) {
LogUtils.w(TAG, "checkAPKValidation: 入参clientSign为空/空白,直接返回校验失败");
callCallbackOnMainThread(callback, false, "应用签名字符串不能为空");
return;
}
if (isStringEmpty(clientHash)) {
LogUtils.w(TAG, "checkAPKValidation: 入参clientHash为空/空白,直接返回校验失败");
callCallbackOnMainThread(callback, false, "APK SHA256哈希字符串不能为空");
return;
}
LogUtils.d(TAG, "checkAPKValidation: 入参校验通过,开始处理网络请求");
// 调用签名/哈希获取方法 // 2. 动态参数URL编码避免特殊字符导致请求解析异常
LogUtils.d(TAG, "checkAPKValidation: 开始获取应用官方签名与APK SHA256哈希"); LogUtils.d(TAG, "checkAPKValidation: 开始对动态参数进行UTF-8 URL编码");
// String clientSign = getOfficialSignBase64(context);
// String clientHash = getApkSHA256Hash(context);
// 获取与服务端对齐的签名
String clientSign = ApkSignUtils.getApkSignAlignedWithServer(context);
// 获取哈希(不变)
String clientHash = ApkSignUtils.getApkSHA256Hash(context);
// 传服务端校验
// 签名/哈希结果校验
if (clientSign == null) {
LogUtils.e(TAG, "checkAPKValidation: 获取应用官方签名失败");
if (callback != null) {
callback.onResult(false, "获取应用签名失败");
}
return;
}
if (clientHash == null) {
LogUtils.e(TAG, "checkAPKValidation: 获取APK SHA256哈希失败");
if (callback != null) {
callback.onResult(false, "获取APK哈希失败");
}
return;
}
LogUtils.d(TAG, "checkAPKValidation: 签名获取成功,哈希获取成功");
// URL编码动态参数
LogUtils.d(TAG, "checkAPKValidation: 开始对动态参数进行URL编码");
String encodeProjectName = urlEncode(projectName); String encodeProjectName = urlEncode(projectName);
String encodeVersionName = urlEncode(versionName); String encodeVersionName = urlEncode(versionName);
String encodeClientSign = urlEncode(clientSign); String encodeClientSign = urlEncode(clientSign);
String encodeClientHash = urlEncode(clientHash); String encodeClientHash = urlEncode(clientHash);
String isDebug = String.valueOf(GlobalApplication.isDebugging());
LogUtils.d(TAG, "checkAPKValidation: 参数编码完成debug标识=" + isDebug);
// 构建请求URL // 3. 构建完整网络校验请求URL
String requestUrl = String.format("%s?isDebug=%s&projectName=%s&versionName=%s&clientSign=%s&clientHash=%s", String requestUrl = String.format("%s?isDebug=%s&projectName=%s&versionName=%s&clientSign=%s&clientHash=%s",
GlobalApplication.getWinbollHost() + CHECK_API_URI, GlobalApplication.getWinbollHost() + CHECK_API_URI,
String.format("%s", GlobalApplication.isDebugging()), isDebug,
encodeProjectName, encodeProjectName,
encodeVersionName, encodeVersionName,
encodeClientSign, encodeClientSign,
encodeClientHash); encodeClientHash);
LogUtils.d(TAG, "checkAPKValidation: 构建校验请求URL=" + requestUrl); LogUtils.d(TAG, "checkAPKValidation: 构建网络校验请求URL=" + requestUrl);
// 发起OKHTTP异步GET请求 // 4. 发起OKHTTP异步GET请求(避免阻塞主线程)
LogUtils.d(TAG, "checkAPKValidation: 发起网络校验异步请求"); LogUtils.d(TAG, "checkAPKValidation: 发起异步网络校验请求");
Request request = new Request.Builder().url(requestUrl).build(); Request request = new Request.Builder().url(requestUrl).build();
sOkHttpClient.newCall(request).enqueue(new Callback() { sOkHttpClient.newCall(request).enqueue(new Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
final String errorMsg = "网络校验请求失败:" + e.getMessage(); String errorMsg = "网络校验请求失败:" + e.getMessage();
LogUtils.e(TAG, "checkAPKValidation: " + errorMsg, e); LogUtils.e(TAG, "checkAPKValidation: " + errorMsg, e);
if (callback != null) { callCallbackOnMainThread(callback, false, errorMsg);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(false, errorMsg);
}
});
}
} }
@Override @Override
public void onResponse(Call call, Response response) throws IOException { public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
// 响应成功解析返回JSON
String responseJson = response.body().string(); String responseJson = response.body().string();
LogUtils.d(TAG, "checkAPKValidation: 网络校验响应JSON=" + responseJson); LogUtils.d(TAG, "checkAPKValidation: 网络校验响应成功,JSON=" + responseJson);
// 解析响应结果 SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
final SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class); boolean isValid = checkResponse != null && checkResponse.isValid();
final boolean isValid = checkResponse != null && checkResponse.isValid(); String msg = checkResponse != null ? checkResponse.getMessage() : "服务端响应解析失败";
final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败"; LogUtils.d(TAG, "checkAPKValidation: 校验结果解析完成isValid=" + isValid + ", 提示信息=" + msg);
LogUtils.d(TAG, "checkAPKValidation: 校验结果解析完成isValid=" + isValid + ", msg=" + msg); callCallbackOnMainThread(callback, isValid, msg);
if (callback != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(isValid, msg);
}
});
}
} else { } else {
final String errorMsg = "网络校验响应失败code=" + response.code(); // 响应失败,返回状态码信息
String errorMsg = "网络校验响应失败,服务端状态码=" + response.code();
LogUtils.e(TAG, "checkAPKValidation: " + errorMsg); LogUtils.e(TAG, "checkAPKValidation: " + errorMsg);
if (callback != null) { callCallbackOnMainThread(callback, false, errorMsg);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(false, errorMsg);
}
});
}
} }
} }
}); });
} }
// ===================================== 内部工具方法 ===================================== // ===================================== 内部工具方法 =====================================
/** /**
* 获取当前应用的APK包文件对象 * 字符串空值/空白校验工具
* @param context 上下文 * @param str 待校验字符串
* @return APK文件File失败返回null * @return true=空/空白false=非空
*/ */
private File getCurrentAppApkFile(Context context) { private boolean isStringEmpty(String str) {
try { return str == null || str.trim().isEmpty();
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
context.getPackageName(), 0
);
String apkPath = appInfo.sourceDir;
File apkFile = new File(apkPath);
return apkFile.exists() && apkFile.isFile() ? apkFile : null;
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getCurrentAppApkFile: 获取应用APK路径失败", e);
return null;
} catch (Exception e) {
LogUtils.e(TAG, "getCurrentAppApkFile: 未知异常", e);
return null;
}
}
/**
* 复刻Android系统Signature的DER编码解析逻辑
* 从CERT.RSA的DER字节中提取与signatures[0].toByteArray()一致的签名字节
* @param derBytes CERT.RSA的DER编码字节
* @return 标准签名字节失败返回null
*/
private byte[] parseDerForAndroidSignature(byte[] derBytes) {
try {
int offset = 0;
if (derBytes == null || derBytes.length < 2 || derBytes[offset++] != 0x30) {
LogUtils.w(TAG, "parseDerForAndroidSignature: DER编码非标准SEQUENCE格式");
return null;
}
// 跳过长度字段
if (derBytes[offset] > 0x80) {
int lenLen = derBytes[offset++] & 0x7F;
offset += lenLen;
} else {
offset++;
}
// 提取签名块字节
while (offset < derBytes.length) {
if (derBytes[offset] == 0x30) {
byte[] sigBytes = new byte[derBytes.length - offset];
System.arraycopy(derBytes, offset, sigBytes, 0, sigBytes.length);
return sigBytes;
}
offset++;
}
LogUtils.w(TAG, "parseDerForAndroidSignature: DER编码中未找到签名块SEQUENCE");
return null;
} catch (Exception e) {
LogUtils.e(TAG, "parseDerForAndroidSignature: 解析DER编码失败", e);
return null;
}
}
/**
* 输入流转字节数组Java7适配无第三方依赖
* @param is 输入流
* @return 字节数组,失败返回空数组
* @throws IOException 流读取异常
*/
private byte[] readStreamToBytes(InputStream is) throws IOException {
if (is == null) return new byte[0];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
byte[] result = bos.toByteArray();
bos.close();
is.close();
return result;
} }
/** /**
* URL编码工具Java7适配UTF-8编码处理特殊字符 * URL编码工具Java7适配UTF-8编码处理特殊字符
* @param content 待编码内容 * @param content 待编码内容
* @return 编码后的字符串,失败返回原内容 * @return 编码后的字符串,编码失败返回原内容
*/ */
private static String urlEncode(String content) { private String urlEncode(String content) {
try { try {
return URLEncoder.encode(content, "UTF-8"); return URLEncoder.encode(content, "UTF-8");
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "urlEncode: 编码失败content=" + content, e); LogUtils.e(TAG, "urlEncode: 字符串编码失败content=" + content, e);
return content; return content;
} }
} }
/** /**
* 从PackageManager获取应用签名SHA1指纹BASE64编码快速获取 * 主线程执行回调(统一处理,避免外部线程切换
* @param context 上下文 * @param callback 回调接口
* @return 签名Base64字符串失败返回null * @param isValid 是否合法
* @param message 提示信息
*/ */
private static String getAppSignFingerprint(Context context) { private void callCallbackOnMainThread(final CheckResultCallback callback,
try { final boolean isValid, final String message) {
PackageManager pm = context.getPackageManager(); if (callback == null) {
PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); LogUtils.w(TAG, "callCallbackOnMainThread: 回调接口为null无需执行");
Signature[] signatures = pkgInfo.signatures; return;
if (signatures == null || signatures.length == 0) { }
LogUtils.w(TAG, "getAppSignFingerprint: 未获取到应用签名"); // 已在主线程直接执行,否则切换主线程
return null; if (Looper.myLooper() == Looper.getMainLooper()) {
} callback.onResult(isValid, message);
MessageDigest md = MessageDigest.getInstance("SHA1"); } else {
md.update(signatures[0].toByteArray()); new Handler(Looper.getMainLooper()).post(new Runnable() {
return Base64.encodeToString(md.digest(), Base64.NO_WRAP); @Override
} catch (PackageManager.NameNotFoundException e) { public void run() {
LogUtils.e(TAG, "getAppSignFingerprint: 获取包信息异常", e); callback.onResult(isValid, message);
} catch (NoSuchAlgorithmException e) { }
LogUtils.e(TAG, "getAppSignFingerprint: 获取SHA1算法异常", e); });
} catch (Exception e) {
LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e);
} }
return null;
} }
// ===================================== 回调接口 ===================================== // ===================================== 校验结果回调接口 =====================================
/** /**
* 校验结果回调接口(主线程调用) * 应用合法性校验结果回调接口(主线程调用)
*/ */
public interface CheckResultCallback { public interface CheckResultCallback {
/** /**
* 校验结果回调 * 校验结果回调方法
* @param isValid 是否合法 * @param isValid 是否合法true=校验通过false=校验失败)
* @param message 校验信息/错误信息 * @param message 校验提示信息(失败时返回错误原因,成功时返回服务端提示)
*/ */
void onResult(boolean isValid, String message); void onResult(boolean isValid, String message);
} }

View File

@@ -2,6 +2,9 @@ package cc.winboll.studio.libappbase.utils;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64; import android.util.Base64;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
@@ -13,164 +16,238 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
/** /**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com> * @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026-01-24 10:00:00 * @CreateTime 2026-01-24 10:00:00
* @LastEditTime 2026-01-24 16:45:00 * @LastEditTime 2026-01-24 22:00:00
* @Describe 客户端签名工具类与服务端APKFileUtils签名/哈希校验逻辑严格对齐,兼容Java7 * @Describe 客户端签名工具类与服务端APKFileUtils签名/哈希校验逻辑严格对齐,Java7实现兼容MT重签名遍历META-INF所有RSA文件增加PackageManager兜底方案
*/ */
public class ApkSignUtils { public class ApkSignUtils {
// ===================================== 全局常量 ===================================== // ===================================== 全局常量定义 =====================================
private static final String TAG = "ApkSignUtils"; private static final String TAG = "ApkSignUtils";
private static final String CERT_RSA_UPPER = "META-INF/CERT.RSA"; // 加密算法常量
private static final String CERT_RSA_LOWER = "META-INF/cert.rsa"; private static final String ALGORITHM_SHA1 = "SHA1";
private static final String SIGN_ALGORITHM_SHA1 = "SHA1"; private static final String ALGORITHM_SHA256 = "SHA-256";
private static final String SIGN_ALGORITHM_SHA256 = "SHA-256"; // 缓冲区大小常量(按业务场景区分)
private static final int BUFFER_SIZE_4K = 4096; private static final int BUFFER_4K = 4096;
private static final int BUFFER_SIZE_8K = 8192; private static final int BUFFER_8K = 8192;
// 签名文件目录与后缀
private static final String META_INF_DIR = "META-INF/";
private static final String RSA_SUFFIX_UPPER = ".RSA";
private static final String RSA_SUFFIX_LOWER = ".rsa";
// ===================================== 对外核心方法 ===================================== // ===================================== 对外核心方法 =====================================
/** /**
* 获取与服务端对齐的签名Base64(核心方法 * 获取与服务端对齐的签名Base64兼容MT重签名
* 直接读取APK内CERT.RSA原始字节 → SHA1摘要 → Base64.NO_WRAP编码,与服务端逻辑完全一致 * 优先逻辑遍历APK内META-INF所有.RSA文件 → 读取第一个有效文件原始字节 → SHA1摘要 → Base64.NO_WRAP
* @param context 上下文用于获取当前应用APK路径 * 兜底逻辑PackageManager获取系统解析的签名 → SHA1摘要 → Base64.NO_WRAP
* @return 签名Base64字符串失败返回null * @param context 上下文用于获取当前应用APK路径/包信息
* @return 签名Base64字符串任意步骤失败返回null
*/ */
public static String getApkSignAlignedWithServer(Context context) { public static String getApkSignAlignedWithServer(Context context) {
LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始获取服务端对齐签名"); LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始执行服务端对齐签名计算兼容MT重签名");
// 入参空值快速校验
if (context == null) { if (context == null) {
LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为直接返回null"); LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为null直接返回null");
return null; return null;
} }
try { // 方案1优先读取APK内META-INF目录下所有RSA文件兼容MT重签名任意命名
// 1. 获取当前应用APK的真实安装路径 String signBase64 = getSignFromApkRsaFile(context);
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); if (signBase64 != null) {
String apkPath = appInfo.sourceDir; LogUtils.d(TAG, "getApkSignAlignedWithServer: 方案1成功APK内读取RSA文件返回签名Base64");
LogUtils.d(TAG, "getApkSignAlignedWithServer: 获取到当前应用APK路径=" + apkPath);
if (apkPath == null || apkPath.trim().isEmpty()) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取APK路径为空获取签名失败");
return null;
}
// 2. 读取APK内的CERT.RSA原始字节流兼容大小写命名
byte[] certRawBytes = readCertRsaRawBytes(apkPath);
if (certRawBytes == null || certRawBytes.length == 0) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 读取CERT.RSA原始字节失败字节数组为空");
return null;
}
LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功读取CERT.RSA原始字节长度=" + certRawBytes.length);
// 3. SHA1摘要计算 + Base64.NO_WRAP编码与服务端完全对齐
MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM_SHA1);
byte[] signDigest = md.digest(certRawBytes);
String signBase64 = Base64.encodeToString(signDigest, Base64.NO_WRAP);
LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功计算服务端对齐签名Base64完成方法执行");
return signBase64; return signBase64;
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取SHA1算法实例失败获取签名失败", e);
} catch (Exception e) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取服务端对齐签名发生未知异常", e);
} }
// 方案2兜底 - PackageManager获取系统解析的应用签名避免APK文件读取失败
signBase64 = getSignFromPackageManager(context);
if (signBase64 != null) {
LogUtils.d(TAG, "getApkSignAlignedWithServer: 方案2成功PackageManager兜底返回签名Base64");
return signBase64;
}
// 所有方案失败
LogUtils.e(TAG, "getApkSignAlignedWithServer: 所有签名获取方案均失败");
return null; return null;
} }
/** /**
* 获取当前运行APK的SHA256哈希值 * 获取当前运行APK的SHA256哈希值兼容重签名APK
* 读取APK文件完整字节流计算SHA256转小写64位16进制字符串服务端校验逻辑一致 * 逻辑:读取APK完整文件字节流 → SHA256摘要 → 转小写64位16进制字符串服务端同款校验逻辑
* @param context 上下文用于获取当前应用APK路径 * @param context 上下文用于获取当前应用APK的真实安装路径
* @return SHA256哈希小写字符串,失败返回null * @return SHA256小写16进制字符串任意步骤失败返回null
*/ */
public static String getApkSHA256Hash(Context context) { public static String getApkSHA256Hash(Context context) {
LogUtils.d(TAG, "getApkSHA256Hash: 方法调用,开始获取APK SHA256哈希"); LogUtils.d(TAG, "getApkSHA256Hash: 方法调用,开始执行APK文件SHA256哈希计算");
// 入参空值快速校验
if (context == null) { if (context == null) {
LogUtils.w(TAG, "getApkSHA256Hash: 入参context为直接返回null"); LogUtils.w(TAG, "getApkSHA256Hash: 入参context为null直接返回null");
return null; return null;
} }
JarFile jarFile = null;
FileInputStream fis = null;
try { try {
// 1. 获取当前应用APK真实安装路径 // 1. 获取当前应用APK真实路径
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo();
String apkPath = appInfo.sourceDir; String apkPath = appInfo.sourceDir;
LogUtils.d(TAG, "getApkSHA256Hash: 获取到当前应用APK路径=" + apkPath); LogUtils.d(TAG, "getApkSHA256Hash: 成功获取APK路径path=" + apkPath);
if (apkPath == null || apkPath.trim().isEmpty()) { if (apkPath == null || apkPath.trim().isEmpty()) {
LogUtils.e(TAG, "getApkSHA256Hash: 获取APK路径为空获取哈希失败"); LogUtils.e(TAG, "getApkSHA256Hash: 获取到的APK路径为空无法读取文件计算哈希");
return null; return null;
} }
// 2. 读取APK文件并计算SHA256哈希 // 2. 读取APK文件并计算SHA256哈希(完善流关闭)
File apkFile = new File(apkPath); File apkFile = new File(apkPath);
MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM_SHA256); MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA256);
FileInputStream fis = new FileInputStream(apkFile); fis = new FileInputStream(apkFile);
byte[] buffer = new byte[BUFFER_SIZE_8K]; byte[] buffer = new byte[BUFFER_8K];
int len; int readLen;
while ((len = fis.read(buffer)) != -1) { while ((readLen = fis.read(buffer)) != -1) {
md.update(buffer, 0, len); md.update(buffer, 0, readLen);
} }
fis.close(); LogUtils.d(TAG, "getApkSHA256Hash: APK文件读取完成开始转换哈希结果");
// 3. 哈希字节转小写64位16进制字符串 // 3. 哈希字节数组转小写64位16进制字符串
byte[] hashBytes = md.digest(); byte[] hashBytes = md.digest();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) { for (byte b : hashBytes) {
sb.append(String.format("%02x", b)); sb.append(String.format("%02x", b));
} }
String sha256Hash = sb.toString(); String sha256Hash = sb.toString();
LogUtils.d(TAG, "getApkSHA256Hash: 成功计算APK SHA256哈希值,完成方法执行"); LogUtils.d(TAG, "getApkSHA256Hash: APK SHA256哈希计算完成,成功返回结果");
return sha256Hash; return sha256Hash;
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败,获取哈希失败", e); LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败", e);
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "getApkSHA256Hash: 获取APK SHA256哈希发生未知异常", e); LogUtils.e(TAG, "getApkSHA256Hash: 计算APK SHA256哈希发生未知异常", e);
} finally {
// 强制关闭流避免重签名APK解析的流泄漏
try {
if (fis != null) fis.close();
if (jarFile != null) jarFile.close();
} catch (IOException e) {
LogUtils.e(TAG, "getApkSHA256Hash: 关闭流资源异常", e);
}
} }
return null; return null;
} }
// ===================================== 内部工具方法 ===================================== // ===================================== 内部核心工具方法(兼容重签名) =====================================
/** /**
* 读取APK内CERT.RSA文件的原始字节流兼容大小写命名CERT.RSA/cert.rsa * 方案1遍历APK内META-INF所有.RSA/.rsa文件读取第一个有效文件计算签名
* @param apkPath 当前应用APK的完整安装路径 * @param context 上下文
* @return CERT.RSA原始字节数组失败返回null * @return 签名Base64失败返回null
* @throws Exception 流读取/APK解析异常
*/ */
private static byte[] readCertRsaRawBytes(String apkPath) throws Exception { private static String getSignFromApkRsaFile(Context context) {
LogUtils.d(TAG, "readCertRsaRawBytes: 方法调用APK路径=" + apkPath); JarFile jarFile = null;
JarFile jarFile = new JarFile(apkPath); InputStream is = null;
JarEntry certEntry = null; try {
// 获取APK路径
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo();
String apkPath = appInfo.sourceDir;
if (apkPath == null || apkPath.trim().isEmpty()) {
LogUtils.w(TAG, "getSignFromApkRsaFile: APK路径为空跳过该方案");
return null;
}
// 优先读取大写命名,找不到则读取小写命名 // 打开APK的JarFile
certEntry = jarFile.getJarEntry(CERT_RSA_UPPER); jarFile = new JarFile(apkPath);
if (certEntry == null) { Enumeration<JarEntry> entries = jarFile.entries();
LogUtils.d(TAG, "readCertRsaRawBytes: 未找到META-INF/CERT.RSA尝试读取META-INF/cert.rsa"); JarEntry targetRsaEntry = null;
certEntry = jarFile.getJarEntry(CERT_RSA_LOWER);
}
// 未找到有效CERT.RSA文件 // 遍历所有条目找到META-INF下第一个.RSA/.rsa文件
if (certEntry == null) { while (entries.hasMoreElements()) {
LogUtils.e(TAG, "readCertRsaRawBytes: APK内未找到CERT.RSA/cert.rsa签名文件"); JarEntry entry = entries.nextElement();
jarFile.close(); String entryName = entry.getName();
// 过滤META-INF目录下 + 以.RSA/.rsa结尾 + 非目录
if (entryName.startsWith(META_INF_DIR) && !entry.isDirectory()
&& (entryName.endsWith(RSA_SUFFIX_UPPER) || entryName.endsWith(RSA_SUFFIX_LOWER))) {
targetRsaEntry = entry;
LogUtils.d(TAG, "getSignFromApkRsaFile: 找到有效签名文件name=" + entryName);
break; // 取第一个有效RSA文件即可
}
}
// 未找到任何RSA文件
if (targetRsaEntry == null) {
LogUtils.w(TAG, "getSignFromApkRsaFile: 未在META-INF找到任何.RSA/.rsa签名文件");
return null;
}
// 读取RSA文件原始字节
is = jarFile.getInputStream(targetRsaEntry);
byte[] certRawBytes = readStreamToBytes(is);
if (certRawBytes == null || certRawBytes.length == 0) {
LogUtils.w(TAG, "getSignFromApkRsaFile: 读取RSA文件字节为空");
return null;
}
// 计算SHA1+Base64
MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA1);
byte[] signDigest = md.digest(certRawBytes);
return Base64.encodeToString(signDigest, Base64.NO_WRAP);
} catch (Exception e) {
LogUtils.e(TAG, "getSignFromApkRsaFile: 从APK内读取RSA文件失败", e);
return null; return null;
} finally {
// 强制关闭所有流
try {
if (is != null) is.close();
if (jarFile != null) jarFile.close();
} catch (IOException e) {
LogUtils.e(TAG, "getSignFromApkRsaFile: 关闭流资源异常", e);
}
} }
// 读取文件原始字节流并关闭流
InputStream is = jarFile.getInputStream(certEntry);
byte[] bytes = readStreamToBytes(is);
is.close();
jarFile.close();
LogUtils.d(TAG, "readCertRsaRawBytes: 成功读取CERT.RSA字节完成方法执行");
return bytes;
} }
/** /**
* 输入流转字节数组与服务端工具方法逻辑完全一致4K缓冲区 * 方案2兜底 - 通过PackageManager获取系统解析的应用签名
* 避免APK文件读取失败如权限、解析问题兼容所有重签名场景
* @param context 上下文
* @return 签名Base64失败返回null
*/
private static String getSignFromPackageManager(Context context) {
try {
// 获取当前应用包信息(包含签名)
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
if (packageInfo == null || packageInfo.signatures == null || packageInfo.signatures.length == 0) {
LogUtils.w(TAG, "getSignFromPackageManager: 未获取到应用签名信息");
return null;
}
// 取第一个签名(重签名后一般只有一个签名)
Signature signature = packageInfo.signatures[0];
byte[] signBytes = signature.toByteArray();
// 计算SHA1+Base64与服务端逻辑对齐
MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA1);
byte[] signDigest = md.digest(signBytes);
return Base64.encodeToString(signDigest, Base64.NO_WRAP);
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getSignFromPackageManager: 包名未找到,无法获取签名", e);
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getSignFromPackageManager: 获取SHA1算法实例失败", e);
} catch (Exception e) {
LogUtils.e(TAG, "getSignFromPackageManager: PackageManager获取签名失败", e);
}
return null;
}
/**
* 输入流转字节数组通用工具方法完善try-finally
* 4K缓冲区适配小文件读取如RSA签名文件保证流资源正常关闭
* @param is 待读取的输入流 * @param is 待读取的输入流
* @return 字节数组流为null返回空字节数组 * @return 转换后的字节数组流为null/读取失败返回空字节数组
* @throws IOException 流读取异常 * @throws IOException 流读取相关异常向上抛出
*/ */
private static byte[] readStreamToBytes(InputStream is) throws IOException { private static byte[] readStreamToBytes(InputStream is) throws IOException {
if (is == null) { if (is == null) {
@@ -178,16 +255,18 @@ public class ApkSignUtils {
return new byte[0]; return new byte[0];
} }
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE_4K]; byte[] buffer = new byte[BUFFER_4K];
int len; int readLen;
while ((len = is.read(buffer)) != -1) { try {
bos.write(buffer, 0, len); while ((readLen = is.read(buffer)) != -1) {
bos.write(buffer, 0, readLen);
}
return bos.toByteArray();
} finally {
// 强制关闭所有流
is.close();
bos.close();
} }
byte[] result = bos.toByteArray();
// 关闭流资源,避免泄漏
is.close();
bos.close();
return result;
} }
} }

View File

@@ -12,71 +12,81 @@ import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import cc.winboll.studio.libappbase.GlobalApplication; 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.DebugHostDialog;
import cc.winboll.studio.libappbase.dialogs.SignGetDialog; import cc.winboll.studio.libappbase.dialogs.AppValidationDialog;
import cc.winboll.studio.libappbase.models.APPInfo; import cc.winboll.studio.libappbase.models.APPInfo;
/** /**
* @Describe AboutView 原生实现关于页面无第三方依赖适配API30抽象通用功能控件邮件/网页跳转)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com> * @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/11 12:23:00 * @CreateTime 2026-01-11 12:23:00
* @LastEditTime 2026/01/20 20:45:00 * @LastEditTime 2026-01-24 20:50:00
* @Describe AboutView 原生实现关于页面无第三方依赖适配API30抽象通用功能控件邮件/网页跳转),支持调试工具入口动态显隐,集成应用正版校验、调试地址配置弹窗
*/ */
public class AboutView extends LinearLayout { public class AboutView extends LinearLayout {
// 全局常量区(标识、回调标识) // ===================================== 全局常量 =====================================
public static final String TAG = "AboutView"; public static final String TAG = "AboutView";
public static final int MSG_APPUPDATE_CHECKED = 0; public static final int MSG_APPUPDATE_CHECKED = 0;
// 固定链接常量 // 固定链接/邮件常量
private static final String WINBOLL_OFFICIAL_HOME = "https://www.winboll.cc"; private static final String WINBOLL_OFFICIAL_HOME = "https://www.winboll.cc";
// 邮件相关常量(统一封装,便于维护)
private static final String EMAIL_TITLE = "联系WinBoLLStudio"; private static final String EMAIL_TITLE = "联系WinBoLLStudio";
private static final String EMAIL_ADDRESS = "studio@winboll.cc"; private static final String EMAIL_ADDRESS = "studio@winboll.cc";
private static final String EMAIL_TYPE = "message/rfc822"; private static final String EMAIL_TYPE = "message/rfc822";
// 布局尺寸常量(统一管理适配多屏幕dp为基准单位 // 布局尺寸常量(dp
private static final int PADDING_LARGE = 32; private static final int PADDING_LARGE = 32;
private static final int PADDING_MID = 16; private static final int PADDING_MID = 16;
private static final int PADDING_SMALL = 8; private static final int PADDING_SMALL = 8;
private static final int ICON_SIZE = 48; private static final int ICON_SIZE = 48;
private static final int ITEM_ICON_SIZE = 24; private static final int ITEM_ICON_SIZE = 24;
// 成员属性区(按 核心依赖→业务配置→视图相关 归类排序,注释清晰) // 服务器默认地址常量
private Context mContext; // 上下文对象,全局复用 private static final String SERVER_DEBUG_HOST = "https://yun-preivew.winboll.cc";
private APPInfo mAPPInfo; // 应用核心信息实体 private static final String SERVER_RELEASE_HOST = "https://yun.winboll.cc";
private OnRequestDevUserInfoAutofillListener mOnRequestDevUserInfoAutofillListener; // 调试信息填充监听
private String mszAppName = ""; // 应用名称 // ===================================== 核心成员属性 =====================================
private String mszAppVersionName = ""; // 应用版本号 // 上下文与业务实体
private String mszAppDescription = ""; // 应用描述文案 private Context mContext;
private String mszHomePage = ""; // 应用主页/APK下载地址 private APPInfo mAPPInfo;
private String mszGitea = ""; // 应用Git源码地址 private OnRequestDevUserInfoAutofillListener mOnRequestDevUserInfoAutofillListener;
private String mszAppGitName = ""; // 应用Git仓库名称
private String mszAppAPKName = ""; // 应用APK基础名称
private String mszAppAPKFolderName = ""; // 应用APK存储文件夹
private String mszCurrentAppPackageName = "";// 当前APK完整文件名
private String mszReleaseAPKName = ""; // 正式版APK完整文件名
private volatile String mszNewestAppPackageName = ""; // 最新版APK文件名支持异步更新
private String mszWinBoLLServerHost = ""; // 服务器地址
private int mnAppIcon = 0; // 应用图标资源ID
private boolean mIsAddDebugTools = false; // 是否启用调试工具标识
private EditText metDevUserName; // 调试用户名输入框
private EditText metDevUserPassword; // 调试密码输入框
// 视图绑定 // 应用基础信息
private String mszAppName = "";
private String mszAppVersionName = "";
private String mszAppDescription = "";
private String mszHomePage = "";
private String mszGitea = "";
private String mszAppGitName = "";
private String mszAppAPKName = "";
private String mszAppAPKFolderName = "";
private String mszCurrentAppPackageName = "";
private String mszReleaseAPKName = "";
private volatile String mszNewestAppPackageName = "";
private String mszWinBoLLServerHost = "";
private int mnAppIcon = 0;
private boolean mIsAddDebugTools = false;
// 调试视图
private EditText metDevUserName;
private EditText metDevUserPassword;
// ===================================== 页面视图控件 =====================================
private ImageView ivAppIcon; private ImageView ivAppIcon;
private TextView tvAppNameVersion; private TextView tvAppNameVersion;
private TextView tvAppDesc; private TextView tvAppDesc;
private LinearLayout llFunctionContainer; private LinearLayout llFunctionContainer;
private ImageButton ibSigngetDialog;
private ImageButton ibWinBoLLHostDialog;
// 构造方法(按 参数从少到多 排序,适配 代码创建+XML引用 场景) // ===================================== 构造方法(按参数从少到多排序 =====================================
public AboutView(Context context) { public AboutView(Context context) {
super(context); super(context);
LogUtils.d(TAG, "AboutView(Context) 构造方法调用,代码创建视图场景"); LogUtils.d(TAG, "AboutView(Context):代码创建视图,执行默认初始化");
this.mContext = context; this.mContext = context;
initDefaultParams(); initDefaultParams();
initViewFromXml(); initViewFromXml();
@@ -84,7 +94,7 @@ public class AboutView extends LinearLayout {
public AboutView(Context context, APPInfo appInfo) { public AboutView(Context context, APPInfo appInfo) {
super(context); super(context);
LogUtils.d(TAG, "AboutView(Context,APPInfo) 构造调用入参APPInfo" + (appInfo == null ? "null" : appInfo.getAppName())); LogUtils.d(TAG, "AboutView(Context,APPInfo)传入应用信息appName=" + (appInfo == null ? "null" : appInfo.getAppName()));
this.mContext = context; this.mContext = context;
this.mAPPInfo = appInfo; this.mAPPInfo = appInfo;
initViewFromXml(); initViewFromXml();
@@ -93,7 +103,7 @@ public class AboutView extends LinearLayout {
public AboutView(Context context, AttributeSet attrs) { public AboutView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
LogUtils.d(TAG, "AboutView(Context,AttributeSet) 构造调用,XML布局引用场景"); LogUtils.d(TAG, "AboutView(Context,AttributeSet)XML布局引用,执行默认初始化");
this.mContext = context; this.mContext = context;
initDefaultParams(); initDefaultParams();
initViewFromXml(); initViewFromXml();
@@ -101,70 +111,22 @@ public class AboutView extends LinearLayout {
public AboutView(Context context, AttributeSet attrs, int defStyleAttr) { public AboutView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
LogUtils.d(TAG, "AboutView(Context,AttributeSet,int) 构造调用,XML布局+样式配置defStyleAttr" + defStyleAttr); LogUtils.d(TAG, "AboutView(Context,AttributeSet,int)XML布局+样式配置defStyleAttr=" + defStyleAttr);
this.mContext = context; this.mContext = context;
initDefaultParams(); initDefaultParams();
initViewFromXml(); 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, mszAppGitName, mszAppVersionName).show(); // 弹出对话框
}
});
ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "签名获取按钮点击弹出SignGetDialog");
new DebugHostDialog(mContext).show(); // 弹出对话框
}
});
}
// 对外公开方法区(供外部调用,职责单一,注释明确)
/** /**
* 一站式初始化所有关于页逻辑,包含参数、信息、视图全流程初始化 * 一站式初始化所有关于页逻辑,包含参数、应用信息、页面视图全流程
*/ */
public void initAll() { public void initAll() {
LogUtils.d(TAG, "initAll() 一站式初始化调用APPInfo是否为空" + (mAPPInfo == null)); LogUtils.d(TAG, "initAll():开始一站式初始化APPInfo是否为空=" + (mAPPInfo == null));
if (mAPPInfo == null) { if (mAPPInfo == null) {
LogUtils.w(TAG, "initAll() 初始化终止APPInfonull,无法获取应用核心信息"); LogUtils.w(TAG, "initAll()初始化终止APPInfonull");
return; return;
} }
// 按初始化流程执行,有序无冗余
initDefaultParams(); initDefaultParams();
initAppBaseInfo(); initAppBaseInfo();
initAppVersionInfo(); initAppVersionInfo();
@@ -172,60 +134,81 @@ public class AboutView extends LinearLayout {
initAppLinkInfo(); initAppLinkInfo();
initReleaseAPKInfo(); initReleaseAPKInfo();
initAboutPageView(); initAboutPageView();
LogUtils.d(TAG, "initAll() 所有初始化流程执行完成"); LogUtils.d(TAG, "initAll()所有初始化流程执行完成");
} }
/** /**
* 重置应用信息并重新初始化关于页,支持动态更新页内容 * 重置应用信息并重新初始化页,支持动态更新关于页内容
* @param appInfo 新的应用信息实体 * @param appInfo 新的应用信息实体
*/ */
public void setAPPInfoAndInit(APPInfo appInfo) { public void setAPPInfoAndInit(APPInfo appInfo) {
LogUtils.d(TAG, "setAPPInfoAndInit() 调用传入新APPInfo" + (appInfo == null ? "null" : appInfo.getAppName())); LogUtils.d(TAG, "setAPPInfoAndInit()重置应用信息appName=" + (appInfo == null ? "null" : appInfo.getAppName()));
this.mAPPInfo = appInfo; this.mAPPInfo = appInfo;
llFunctionContainer.removeAllViews(); if (llFunctionContainer != null) llFunctionContainer.removeAllViews();
initAll(); initAll();
LogUtils.d(TAG, "setAPPInfoAndInit() 应用信息重置+页面重构完成"); LogUtils.d(TAG, "setAPPInfoAndInit()应用信息重置+页面重构完成");
} }
/** /**
* 设置应用信息兼容旧调用逻辑,设置后自动重构页面 * 设置应用信息兼容旧调用逻辑,设置后自动重构页面
* @param appInfo 应用核心信息实体 * @param appInfo 应用核心信息实体
*/ */
public void setAPPInfo(APPInfo appInfo) { public void setAPPInfo(APPInfo appInfo) {
LogUtils.d(TAG, "setAPPInfo() 调用传入APPInfo" + (appInfo == null ? "null" : appInfo.getAppName())); LogUtils.d(TAG, "setAPPInfo()设置应用信息appName=" + (appInfo == null ? "null" : appInfo.getAppName()));
this.mAPPInfo = appInfo; this.mAPPInfo = appInfo;
llFunctionContainer.removeAllViews(); if (llFunctionContainer != null) llFunctionContainer.removeAllViews();
initAll(); initAll();
} }
/** /**
* 设置调试信息自动填充监听,用于调试场景的信息回调 * 设置调试信息自动填充监听,调试场景回调使用
* @param l 监听回调接口实现 * @param l 监听回调接口实现
*/ */
public void setOnRequestDevUserInfoAutofillListener(OnRequestDevUserInfoAutofillListener l) { public void setOnRequestDevUserInfoAutofillListener(OnRequestDevUserInfoAutofillListener l) {
LogUtils.d(TAG, "setOnRequestDevUserInfoAutofillListener() 调试监听设置完成"); LogUtils.d(TAG, "setOnRequestDevUserInfoAutofillListener():设置调试信息填充监听完成");
this.mOnRequestDevUserInfoAutofillListener = l; this.mOnRequestDevUserInfoAutofillListener = l;
} }
// 内部初始化方法区(按 基础→业务→视图 流程排序,单一职责) // ===================================== 内部初始化方法 =====================================
/** /**
* 初始化默认兜底参数,防止空指针,为后续初始化做基础铺垫 * 初始化默认兜底参数,防止空指针,为后续初始化做基础铺垫
*/ */
private void initDefaultParams() { private void initDefaultParams() {
LogUtils.d(TAG, "initDefaultParams() 执行默认参数初始化"); LogUtils.d(TAG, "initDefaultParams():开始初始化默认参数");
mszWinBoLLServerHost = GlobalApplication.isDebugging() ? "https://yun-preivew.winboll.cc" : "https://yun.winboll.cc"; mszWinBoLLServerHost = GlobalApplication.isDebugging() ? SERVER_DEBUG_HOST : SERVER_RELEASE_HOST;
mnAppIcon = mnAppIcon == 0 ? R.drawable.ic_winboll : mnAppIcon; mnAppIcon = (mnAppIcon == 0) ? R.drawable.ic_winboll : mnAppIcon;
mIsAddDebugTools = false; mIsAddDebugTools = false;
LogUtils.d(TAG, "initDefaultParams() 完成,默认服务器地址" + mszWinBoLLServerHost + "默认图标ID" + mnAppIcon); LogUtils.d(TAG, "initDefaultParams():默认参数初始化完成,服务器地址=" + mszWinBoLLServerHost + "应用图标ID=" + mnAppIcon);
}
/**
* 加载XML布局并绑定所有视图控件初始化按钮点击事件
*/
private void initViewFromXml() {
LogUtils.d(TAG, "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():布局加载+控件绑定+事件初始化完成");
} }
/** /**
* 从APPInfo实体读取应用基础核心配置赋值到本地属性 * 从APPInfo实体读取应用基础核心配置赋值到本地属性
*/ */
private void initAppBaseInfo() { private void initAppBaseInfo() {
LogUtils.d(TAG, "initAppBaseInfo() 读取APPInfo基础配置"); LogUtils.d(TAG, "initAppBaseInfo():开始读取APPInfo基础配置");
if (mAPPInfo == null) { if (mAPPInfo == null) {
LogUtils.w(TAG, "initAppBaseInfo() 跳过执行APPInfonull"); LogUtils.w(TAG, "initAppBaseInfo()跳过执行APPInfonull");
return; return;
} }
mszAppName = mAPPInfo.getAppName() == null ? "" : mAPPInfo.getAppName(); mszAppName = mAPPInfo.getAppName() == null ? "" : mAPPInfo.getAppName();
@@ -233,44 +216,44 @@ public class AboutView extends LinearLayout {
mszAppAPKName = mAPPInfo.getAppAPKName() == null ? "" : mAPPInfo.getAppAPKName(); mszAppAPKName = mAPPInfo.getAppAPKName() == null ? "" : mAPPInfo.getAppAPKName();
mszAppGitName = mAPPInfo.getAppGitName() == null ? "" : mAPPInfo.getAppGitName(); mszAppGitName = mAPPInfo.getAppGitName() == null ? "" : mAPPInfo.getAppGitName();
mszAppDescription = mAPPInfo.getAppDescription() == null ? "" : mAPPInfo.getAppDescription(); mszAppDescription = mAPPInfo.getAppDescription() == null ? "" : mAPPInfo.getAppDescription();
mnAppIcon = mAPPInfo.getAppIcon() != 0 ? mAPPInfo.getAppIcon() : mnAppIcon; mnAppIcon = (mAPPInfo.getAppIcon() != 0) ? mAPPInfo.getAppIcon() : mnAppIcon;
mIsAddDebugTools = mAPPInfo.isAddDebugTools(); mIsAddDebugTools = mAPPInfo.isAddDebugTools();
LogUtils.d(TAG, "initAppBaseInfo() 读取完成,应用名" + mszAppName + ",调试开关" + mIsAddDebugTools); LogUtils.d(TAG, "initAppBaseInfo():基础配置读取完成,应用名=" + mszAppName + ",调试开关=" + mIsAddDebugTools);
} }
/** /**
* 初始化应用版本信息,从包管理中获取当前应用版本号 * 从包管理中获取当前应用版本号,初始化版本相关信息
*/ */
private void initAppVersionInfo() { private void initAppVersionInfo() {
LogUtils.d(TAG, "initAppVersionInfo() 初始化应用版本信息"); LogUtils.d(TAG, "initAppVersionInfo():开始初始化应用版本信息");
try { try {
mszAppVersionName = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionName; mszAppVersionName = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
LogUtils.d(TAG, "initAppVersionInfo() 获取版本号失败默认赋值unknown", e); LogUtils.e(TAG, "initAppVersionInfo()获取版本号失败默认赋值unknown", e);
mszAppVersionName = "unknown"; mszAppVersionName = "unknown";
} }
mszCurrentAppPackageName = String.format("%s_%s.apk", mszAppVersionName, mszAppVersionName); mszCurrentAppPackageName = String.format("%s_%s.apk", mszAppVersionName, mszAppVersionName);
LogUtils.d(TAG, "initAppVersionInfo() 完成,版本号" + mszAppVersionName + "当前APK名" + mszCurrentAppPackageName); LogUtils.d(TAG, "initAppVersionInfo():版本信息初始化完成,版本号=" + mszAppVersionName + "当前APK名=" + mszCurrentAppPackageName);
} }
/** /**
* 初始化服务器相关配置,预留扩展接口 * 初始化服务器相关配置,预留扩展接口
*/ */
private void initServerConfig() { private void initServerConfig() {
LogUtils.d(TAG, "initServerConfig() 服务器配置初始化预留扩展"); LogUtils.d(TAG, "initServerConfig()服务器配置初始化预留扩展接口");
} }
/** /**
* 初始化应用相关链接(主页+Git源码地址动态拼接Git地址 * 初始化应用相关链接(主页+Git源码地址根据分支配置动态拼接Git地址
*/ */
private void initAppLinkInfo() { private void initAppLinkInfo() {
LogUtils.d(TAG, "initAppLinkInfo() 初始化应用链接信息"); LogUtils.d(TAG, "initAppLinkInfo():开始初始化应用链接信息");
if (mAPPInfo == null) { if (mAPPInfo == null) {
LogUtils.w(TAG, "initAppLinkInfo() 跳过执行APPInfonull"); LogUtils.w(TAG, "initAppLinkInfo()跳过执行APPInfonull");
return; return;
} }
mszHomePage = mAPPInfo.getAppHomePage() == null ? "" : mAPPInfo.getAppHomePage(); mszHomePage = mAPPInfo.getAppHomePage() == null ? "" : mAPPInfo.getAppHomePage();
// 分场景拼接Git地址兼容无分支配置场景 // 拼接Git地址兼容无分支配置场景
if (mAPPInfo.getAppGitAPPBranch() == null || mAPPInfo.getAppGitAPPBranch().trim().isEmpty()) { if (mAPPInfo.getAppGitAPPBranch() == null || mAPPInfo.getAppGitAPPBranch().trim().isEmpty()) {
mszGitea = String.format("https://gitea.winboll.cc/%s/%s", mAPPInfo.getAppGitOwner(), mszAppGitName); mszGitea = String.format("https://gitea.winboll.cc/%s/%s", mAPPInfo.getAppGitOwner(), mszAppGitName);
} else { } else {
@@ -278,31 +261,31 @@ public class AboutView extends LinearLayout {
mAPPInfo.getAppGitOwner(), mszAppGitName, mAPPInfo.getAppGitOwner(), mszAppGitName,
mAPPInfo.getAppGitAPPBranch(), mAPPInfo.getAppGitAPPSubProjectFolder()); mAPPInfo.getAppGitAPPBranch(), mAPPInfo.getAppGitAPPSubProjectFolder());
} }
LogUtils.d(TAG, "initAppLinkInfo() 完成,应用主页" + mszHomePage + "Git地址" + mszGitea); LogUtils.d(TAG, "initAppLinkInfo():链接信息初始化完成,应用主页=" + mszHomePage + "Git地址=" + mszGitea);
} }
/** /**
* 初始化正式版APK信息去除beta后缀适配正式包命名规范 * 初始化正式版APK信息去除beta后缀适配正式包命名规范
*/ */
private void initReleaseAPKInfo() { private void initReleaseAPKInfo() {
LogUtils.d(TAG, "initReleaseAPKInfo() 初始化正式版APK信息"); LogUtils.d(TAG, "initReleaseAPKInfo():开始初始化正式版APK信息");
String szReleaseAppVersionName = "unknown"; String szReleaseAppVersionName = "unknown";
try { try {
String szSubBetaSuffix = subBetaSuffix(mContext.getPackageName()); String szSubBetaSuffix = subBetaSuffix(mContext.getPackageName());
szReleaseAppVersionName = mContext.getPackageManager().getPackageInfo(szSubBetaSuffix, 0).versionName; szReleaseAppVersionName = mContext.getPackageManager().getPackageInfo(szSubBetaSuffix, 0).versionName;
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
LogUtils.d(TAG, "initReleaseAPKInfo() 获取正式版版本号失败", e); LogUtils.e(TAG, "initReleaseAPKInfo()获取正式版版本号失败", e);
} }
mszReleaseAPKName = String.format("%s_%s.apk", mszAppAPKName, szReleaseAppVersionName); mszReleaseAPKName = String.format("%s_%s.apk", mszAppAPKName, szReleaseAppVersionName);
LogUtils.d(TAG, "initReleaseAPKInfo() 完成,正式版APK名:" + mszReleaseAPKName); LogUtils.d(TAG, "initReleaseAPKInfo()正式版APK信息初始化完成APK名=" + mszReleaseAPKName);
} }
/** /**
* 核心视图组装:赋值基础信息+添加功能项 * 核心视图组装:赋值基础信息到控件,添加通用功能项到容器
*/ */
private void initAboutPageView() { private void initAboutPageView() {
LogUtils.d(TAG, "initAboutPageView() 开始组装关于页视图"); LogUtils.d(TAG, "initAboutPageView()开始组装关于页视图");
// 基础信息赋值 // 赋值基础信息
ivAppIcon.setImageResource(mnAppIcon); ivAppIcon.setImageResource(mnAppIcon);
tvAppNameVersion.setText(String.format("%s %s", mszAppName, mszAppVersionName)); tvAppNameVersion.setText(String.format("%s %s", mszAppName, mszAppVersionName));
if (mszAppDescription.isEmpty()) { if (mszAppDescription.isEmpty()) {
@@ -311,8 +294,7 @@ public class AboutView extends LinearLayout {
tvAppDesc.setVisibility(VISIBLE); tvAppDesc.setVisibility(VISIBLE);
tvAppDesc.setText(mszAppDescription); tvAppDesc.setText(mszAppDescription);
} }
// 添加通用功能项
// 通用功能控件:网页跳转类+邮件类,复用抽象控件
addFunctionView(new WebJumpFunctionItemView(mContext, "WinBoLL 主页", WINBOLL_OFFICIAL_HOME, 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)); addFunctionView(new EmailFunctionItemView(mContext, "联系邮箱", "WinBoLLStudio<studio@winboll.cc>", R.drawable.ic_winboll));
if (!mszHomePage.isEmpty()) { if (!mszHomePage.isEmpty()) {
@@ -321,19 +303,46 @@ public class AboutView extends LinearLayout {
if (!mszGitea.isEmpty()) { if (!mszGitea.isEmpty()) {
addFunctionView(new WebJumpFunctionItemView(mContext, "应用Git源码地址", mszGitea, R.drawable.ic_winboll)); addFunctionView(new WebJumpFunctionItemView(mContext, "应用Git源码地址", mszGitea, R.drawable.ic_winboll));
} }
LogUtils.d(TAG, "initAboutPageView() 视图组装完成,功能项加载完毕"); LogUtils.d(TAG, "initAboutPageView()视图组装完成,功能项加载完毕");
} }
// 添加功能项到容器 // ===================================== 内部工具/事件方法 =====================================
/**
* 绑定功能按钮点击事件,处理正版校验、调试地址配置弹窗唤起
*/
private void setBtnClickListener() {
LogUtils.d(TAG, "setBtnClickListener():开始绑定功能按钮点击事件");
// 正版校验弹窗
ibSigngetDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "ibSigngetDialog onClick唤起应用正版校验弹窗");
new AppValidationDialog(mContext, mszAppGitName, mszAppVersionName).show();
}
});
// 调试地址配置弹窗
ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "ibWinBoLLHostDialog onClick唤起调试地址配置弹窗");
new DebugHostDialog(mContext).show();
}
});
LogUtils.d(TAG, "setBtnClickListener():功能按钮点击事件绑定完成");
}
/**
* 添加功能项视图到容器,统一设置间距
* @param view 功能项视图
*/
private void addFunctionView(View view) { private void addFunctionView(View view) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
params.topMargin = dp2px(PADDING_SMALL); params.topMargin = dp2px(PADDING_SMALL);
llFunctionContainer.addView(view, params); llFunctionContainer.addView(view, params);
} }
// 工具方法区(通用工具+业务工具,静态优先,便于复用)
/** /**
* dppx 工具方法,适配不同屏幕密度,保证布局一致性 * dppx工具方法适配不同屏幕密度保证布局一致性
* @param dpValue dp单位尺寸 * @param dpValue dp单位尺寸
* @return 转换后的px单位尺寸 * @return 转换后的px单位尺寸
*/ */
@@ -348,17 +357,20 @@ public class AboutView extends LinearLayout {
* @return 去除beta后缀后的正式包名 * @return 去除beta后缀后的正式包名
*/ */
public static String subBetaSuffix(String input) { public static String subBetaSuffix(String input) {
LogUtils.d(TAG, "subBetaSuffix() 执行包名beta后缀去除原始包名" + input); LogUtils.d(TAG, "subBetaSuffix()执行包名beta后缀去除原始包名=" + input);
if (input != null && input.endsWith(".beta")) { if (input != null && input.endsWith(".beta")) {
String result = input.substring(0, input.length() - ".beta".length()); String result = input.substring(0, input.length() - ".beta".length());
LogUtils.d(TAG, "subBetaSuffix() 处理成功,正式包名" + result); LogUtils.d(TAG, "subBetaSuffix()处理成功,正式包名=" + result);
return result; return result;
} }
LogUtils.d(TAG, "subBetaSuffix() 无需处理包名不含beta后缀"); LogUtils.d(TAG, "subBetaSuffix()无需处理包名不含beta后缀");
return input == null ? "" : input; return input == null ? "" : input;
} }
// 内部抽象通用功能项基类 - 统一样式,减少冗余 // ===================================== 内部抽象通用功能项基类 =====================================
/**
* 通用功能项基类,统一样式、布局、视图构建,减少冗余代码
*/
private abstract class BaseFunctionItemView extends LinearLayout implements OnClickListener { private abstract class BaseFunctionItemView extends LinearLayout implements OnClickListener {
protected Context mItemContext; protected Context mItemContext;
protected String mTitle; protected String mTitle;
@@ -376,7 +388,9 @@ public class AboutView extends LinearLayout {
setOnClickListener(this); setOnClickListener(this);
} }
// 统一布局配置 /**
* 统一初始化功能项布局属性
*/
private void initItemLayout() { private void initItemLayout() {
setOrientation(HORIZONTAL); setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL); setGravity(Gravity.CENTER_VERTICAL);
@@ -386,7 +400,9 @@ public class AboutView extends LinearLayout {
setBackgroundResource(android.R.drawable.list_selector_background); setBackgroundResource(android.R.drawable.list_selector_background);
} }
// 统一视图构建 /**
* 统一构建功能项视图(左侧图标+右侧标题/内容)
*/
private void initItemViews() { private void initItemViews() {
// 左侧图标 // 左侧图标
if (mIconRes != 0) { if (mIconRes != 0) {
@@ -397,20 +413,17 @@ public class AboutView extends LinearLayout {
ivIcon.setImageResource(mIconRes); ivIcon.setImageResource(mIconRes);
addView(ivIcon); addView(ivIcon);
} }
// 右侧文本容器 // 右侧文本容器
LinearLayout llText = new LinearLayout(mItemContext); LinearLayout llText = new LinearLayout(mItemContext);
llText.setOrientation(VERTICAL); llText.setOrientation(VERTICAL);
llText.setLayoutParams(new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1.0f)); llText.setLayoutParams(new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1.0f));
addView(llText); addView(llText);
// 标题 // 标题
TextView tvTitle = new TextView(mItemContext); TextView tvTitle = new TextView(mItemContext);
tvTitle.setText(mTitle); tvTitle.setText(mTitle);
tvTitle.setTextSize(16); tvTitle.setTextSize(16);
tvTitle.setTextColor(mItemContext.getResources().getColor(R.color.gray_900)); tvTitle.setTextColor(mItemContext.getResources().getColor(R.color.gray_900));
llText.addView(tvTitle); llText.addView(tvTitle);
// 内容 // 内容
TextView tvContent = new TextView(mItemContext); TextView tvContent = new TextView(mItemContext);
tvContent.setText(mContent); tvContent.setText(mContent);
@@ -420,11 +433,17 @@ public class AboutView extends LinearLayout {
llText.addView(tvContent); llText.addView(tvContent);
} }
// 子类指定内容文本颜色 /**
* 子类抽象方法:指定内容文本颜色
* @return 颜色值
*/
protected abstract int getContentTextColor(); protected abstract int getContentTextColor();
} }
// 邮件功能控件 - 专属邮件唤起逻辑 // ===================================== 内部邮件功能项子类 =====================================
/**
* 邮件类功能控件,实现专属邮件唤起逻辑,双方案兼容(纯邮件客户端/通用邮件应用)
*/
private class EmailFunctionItemView extends BaseFunctionItemView { private class EmailFunctionItemView extends BaseFunctionItemView {
public EmailFunctionItemView(Context context, String title, String content, int iconRes) { public EmailFunctionItemView(Context context, String title, String content, int iconRes) {
super(context, title, content, iconRes); super(context, title, content, iconRes);
@@ -437,36 +456,37 @@ public class AboutView extends LinearLayout {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "EmailFunctionItemView onClick 触发邮件唤起"); LogUtils.d(TAG, "EmailFunctionItemView onClick触发邮件唤起逻辑");
// 方案邮件唤起逻辑 // 方案1纯邮件客户端唤起
Intent emailIntent = new Intent(Intent.ACTION_SENDTO); Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
emailIntent.setData(Uri.parse("mailto:" + EMAIL_ADDRESS)); emailIntent.setData(Uri.parse("mailto:" + EMAIL_ADDRESS));
emailIntent.putExtra(Intent.EXTRA_SUBJECT, EMAIL_TITLE); emailIntent.putExtra(Intent.EXTRA_SUBJECT, EMAIL_TITLE);
emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (emailIntent.resolveActivity(mItemContext.getPackageManager()) != null) { if (emailIntent.resolveActivity(mItemContext.getPackageManager()) != null) {
mItemContext.startActivity(emailIntent); mItemContext.startActivity(emailIntent);
LogUtils.d(TAG, "邮件唤起成功:系统纯邮件客户端"); LogUtils.d(TAG, "EmailFunctionItemView纯邮件客户端唤起成功");
return; return;
} }
// 方案2通用邮件应用兜底
Intent fallbackIntent = new Intent(Intent.ACTION_SEND); Intent fallbackIntent = new Intent(Intent.ACTION_SEND);
fallbackIntent.setType(EMAIL_TYPE); fallbackIntent.setType(EMAIL_TYPE);
fallbackIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{EMAIL_ADDRESS}); fallbackIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{EMAIL_ADDRESS});
fallbackIntent.putExtra(Intent.EXTRA_SUBJECT, EMAIL_TITLE); fallbackIntent.putExtra(Intent.EXTRA_SUBJECT, EMAIL_TITLE);
fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (fallbackIntent.resolveActivity(mItemContext.getPackageManager()) != null) { if (fallbackIntent.resolveActivity(mItemContext.getPackageManager()) != null) {
mItemContext.startActivity(fallbackIntent); mItemContext.startActivity(fallbackIntent);
LogUtils.d(TAG, "邮件唤起成功:通用邮件应用"); LogUtils.d(TAG, "EmailFunctionItemView:通用邮件应用唤起成功");
} else { } else {
ToastUtils.show("未找到可发送邮件的应用"); ToastUtils.show("未找到可发送邮件的应用");
LogUtils.w(TAG, "邮件唤起失败无可用邮件相关应用"); LogUtils.w(TAG, "EmailFunctionItemView邮件唤起失败无可用邮件应用");
} }
} }
} }
// 网页跳转功能控件 - 专属网页跳转逻辑 // ===================================== 内部网页跳转功能项子类 =====================================
/**
* 网页跳转类功能控件,实现专属网页唤起逻辑,包含空地址校验、异常捕获
*/
private class WebJumpFunctionItemView extends BaseFunctionItemView { private class WebJumpFunctionItemView extends BaseFunctionItemView {
public WebJumpFunctionItemView(Context context, String title, String content, int iconRes) { public WebJumpFunctionItemView(Context context, String title, String content, int iconRes) {
super(context, title, content, iconRes); super(context, title, content, iconRes);
@@ -479,25 +499,28 @@ public class AboutView extends LinearLayout {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
LogUtils.d(TAG, "WebJumpFunctionItemView onClick 触发网页跳转,地址" + mContent); LogUtils.d(TAG, "WebJumpFunctionItemView onClick触发网页跳转,地址=" + mContent);
if (mContent.isEmpty()) { if (mContent.isEmpty()) {
ToastUtils.show("跳转地址为空"); ToastUtils.show("跳转地址为空");
LogUtils.w(TAG, "网页跳转失败地址为空"); LogUtils.w(TAG, "WebJumpFunctionItemView网页跳转失败地址为空");
return; return;
} }
try { try {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(mContent)); Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(mContent));
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mItemContext.startActivity(browserIntent); mItemContext.startActivity(browserIntent);
LogUtils.d(TAG, "网页跳转成功"); LogUtils.d(TAG, "WebJumpFunctionItemView网页跳转成功");
} catch (Exception e) { } catch (Exception e) {
LogUtils.d(TAG, "网页跳转失败,异常捕获", e); LogUtils.e(TAG, "WebJumpFunctionItemView网页跳转失败", e);
ToastUtils.show("链接无法打开"); ToastUtils.show("链接无法打开");
} }
} }
} }
// 内部接口区(置于类末尾,逻辑闭环) // ===================================== 内部回调接口 =====================================
/**
* 调试信息自动填充回调接口
*/
public interface OnRequestDevUserInfoAutofillListener { public interface OnRequestDevUserInfoAutofillListener {
void requestAutofill(EditText etDevUserName, EditText etDevUserPassword); void requestAutofill(EditText etDevUserName, EditText etDevUserPassword);
} }