diff --git a/appbase/build.properties b/appbase/build.properties index 51788ff..8b9ee84 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Jan 24 04:15:06 GMT 2026 +#Sat Jan 24 04:27:31 GMT 2026 stageCount=8 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.7 -buildCount=28 +buildCount=29 baseBetaVersion=15.15.8 diff --git a/libappbase/build.properties b/libappbase/build.properties index 51788ff..8b9ee84 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Jan 24 04:15:06 GMT 2026 +#Sat Jan 24 04:27:31 GMT 2026 stageCount=8 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.7 -buildCount=28 +buildCount=29 baseBetaVersion=15.15.8 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/AppValidationDialog.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/AppValidationDialog.java new file mode 100644 index 0000000..d249b34 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/AppValidationDialog.java @@ -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 + * @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(); + } +} + diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/SignGetDialog.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/SignGetDialog.java deleted file mode 100644 index 9e73793..0000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/SignGetDialog.java +++ /dev/null @@ -1,152 +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.ApkSignUtils; - -/** - * @Describe 签名显示+正版校验对话框 - * @Author 豆包&ZhanGSKen - * @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; - String clientSign; - String clientHash; - - 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); // 点击外部可关闭 - - // 获取与服务端对齐的签名 - this.clientSign = ApkSignUtils.getApkSignAlignedWithServer(this.mContext); - // 获取哈希(不变) - this.clientHash = ApkSignUtils.getApkSHA256Hash(this.mContext); - - initView(); - initSignAndCheck(); // 获取签名+正版校验 - } - - private void initView() { - etSignFingerprint = findViewById(R.id.et_sign_fingerprint); - tvAuthResult = findViewById(R.id.tv_auth_result); - // 输入框只读,方便复制 - etSignFingerprint.setEnabled(false); - etSignFingerprint.setText(convertSignToBitArrayWithWrap(this.clientSign)); - } - - // 核心:获取签名+调用APPUtils校验 - private void initSignAndCheck() { - // 2. 正版校验+显示结果 - // 调用处直接删除base64SignFingerprint参数即可 - new APPUtils().checkAPKValidation( - mContext, - this.projectName, - this.versionName, - this.clientSign, - this.clientHash, - 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); -// } -} - diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/APPUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/APPUtils.java index a02356e..3e13aac 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/APPUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/APPUtils.java @@ -1,10 +1,6 @@ package cc.winboll.studio.libappbase.utils; 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.Looper; import android.util.Base64; @@ -15,16 +11,10 @@ import cc.winboll.studio.libappbase.models.SignCheckResponse; import com.google.gson.Gson; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; import okhttp3.Call; import okhttp3.Callback; @@ -35,278 +25,173 @@ import okhttp3.Response; /** * @Author 豆包&ZhanGSKen * @CreateTime 2026-01-20 19:17:00 - * @LastEditTime 2026-01-24 02:18:00 - * @Describe APPUtils 应用包名、签名校验工具类(OKHTTP网络校验版,兼容Java7,含URL编码+APK包签名+SHA256哈希校验) + * @LastEditTime 2026-01-24 17:58:00 + * @Describe APPUtils 应用合法性校验工具类(OKHTTP网络校验版,兼容Java7) + * 对外传入签名/哈希值,拼接调试标识后发起网络校验,主线程返回校验结果 */ 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客户端单例(复用连接,避免资源浪费) + private static final OkHttpClient sOkHttpClient = new OkHttpClient(); + // Gson解析单例(全局复用,提高解析效率) + private static final Gson sGson = new Gson(); - // ===================================== 对外核心方法 ===================================== + // ===================================== 对外核心校验方法 ===================================== /** - * 检查应用合法性(签名校验+APK哈希校验+网络接口校验) - * @param context 上下文 - * @param projectName 项目名称 - * @param versionName 应用版本名 - * @param callback 校验结果回调(主线程回调) + * 检查应用合法性(外部传入签名+哈希,拼接调试标识发起网络校验) + * @param context 上下文,用于主线程回调 + * @param projectName 项目名称(服务端区分项目标识) + * @param versionName 应用版本名(服务端版本校验) + * @param clientSign 外部计算的应用签名字符串(Base64) + * @param clientHash 外部计算的APK SHA256哈希字符串(小写16进制) + * @param 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); - // 空参校验 + public void checkAPKValidation(Context context, String projectName, String versionName, + String clientSign, String clientHash, final CheckResultCallback callback) { + // 方法调用+全量入参调试日志 + LogUtils.d(TAG, "checkAPKValidation: 方法调用,入参-> projectName=" + projectName + + ", versionName=" + versionName + ", clientSign=" + clientSign + ", clientHash=" + clientHash); + + // 1. 核心入参空值校验(快速失败) if (context == null) { - LogUtils.w(TAG, "checkAPKValidation: 入参context为空,跳过校验"); - if (callback != null) { - callback.onResult(false, "context为空"); - } + LogUtils.w(TAG, "checkAPKValidation: 入参context为空,直接返回校验失败"); + callCallbackOnMainThread(callback, false, "上下文对象不能为空"); return; } - if (projectName == null || projectName.trim().isEmpty()) { - LogUtils.w(TAG, "checkAPKValidation: 入参projectName为空,跳过校验"); - if (callback != null) { - callback.onResult(false, "projectName为空"); - } + if (isStringEmpty(projectName)) { + LogUtils.w(TAG, "checkAPKValidation: 入参projectName为空/空白,直接返回校验失败"); + callCallbackOnMainThread(callback, false, "项目名称不能为空"); return; } - if (versionName == null || versionName.trim().isEmpty()) { - LogUtils.w(TAG, "checkAPKValidation: 入参versionName为空,跳过校验"); - if (callback != null) { - callback.onResult(false, "versionName为空"); - } + if (isStringEmpty(versionName)) { + LogUtils.w(TAG, "checkAPKValidation: 入参versionName为空/空白,直接返回校验失败"); + callCallbackOnMainThread(callback, false, "应用版本名不能为空"); 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: 入参校验通过,开始处理网络请求"); - // 调用签名/哈希获取方法 - LogUtils.d(TAG, "checkAPKValidation: 开始获取应用官方签名与APK SHA256哈希"); -// String clientSign = getOfficialSignBase64(context); -// String clientHash = 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编码"); + // 2. 动态参数URL编码(避免特殊字符导致请求解析异常) + LogUtils.d(TAG, "checkAPKValidation: 开始对动态参数进行UTF-8 URL编码"); String encodeProjectName = urlEncode(projectName); String encodeVersionName = urlEncode(versionName); String encodeClientSign = urlEncode(clientSign); 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", GlobalApplication.getWinbollHost() + CHECK_API_URI, - String.format("%s", GlobalApplication.isDebugging()), + isDebug, encodeProjectName, encodeVersionName, encodeClientSign, encodeClientHash); - LogUtils.d(TAG, "checkAPKValidation: 构建校验请求URL=" + requestUrl); + LogUtils.d(TAG, "checkAPKValidation: 构建网络校验请求URL=" + requestUrl); - // 发起OKHTTP异步GET请求 - LogUtils.d(TAG, "checkAPKValidation: 发起网络校验异步请求"); + // 4. 发起OKHTTP异步GET请求(避免阻塞主线程) + LogUtils.d(TAG, "checkAPKValidation: 发起异步网络校验请求"); 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(); + String errorMsg = "网络校验请求失败:" + e.getMessage(); LogUtils.e(TAG, "checkAPKValidation: " + errorMsg, e); - if (callback != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - callback.onResult(false, errorMsg); - } - }); - } + callCallbackOnMainThread(callback, false, errorMsg); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful() && response.body() != null) { + // 响应成功,解析返回JSON String responseJson = response.body().string(); - LogUtils.d(TAG, "checkAPKValidation: 网络校验响应JSON=" + responseJson); - // 解析响应结果 - final SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class); - final boolean isValid = checkResponse != null && checkResponse.isValid(); - final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败"; - LogUtils.d(TAG, "checkAPKValidation: 校验结果解析完成,isValid=" + isValid + ", msg=" + msg); - if (callback != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - callback.onResult(isValid, msg); - } - }); - } + LogUtils.d(TAG, "checkAPKValidation: 网络校验响应成功,JSON=" + responseJson); + SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class); + boolean isValid = checkResponse != null && checkResponse.isValid(); + String msg = checkResponse != null ? checkResponse.getMessage() : "服务端响应解析失败"; + LogUtils.d(TAG, "checkAPKValidation: 校验结果解析完成,isValid=" + isValid + ", 提示信息=" + msg); + callCallbackOnMainThread(callback, isValid, msg); } else { - final String errorMsg = "网络校验响应失败,code=" + response.code(); + // 响应失败,返回状态码信息 + String errorMsg = "网络校验响应失败,服务端状态码=" + response.code(); LogUtils.e(TAG, "checkAPKValidation: " + errorMsg); - if (callback != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - callback.onResult(false, errorMsg); - } - }); - } + callCallbackOnMainThread(callback, false, errorMsg); } } }); } - // ===================================== 内部工具方法 ===================================== /** - * 获取当前应用的APK包文件对象 - * @param context 上下文 - * @return APK文件File,失败返回null + * 字符串空值/空白校验工具 + * @param str 待校验字符串 + * @return true=空/空白,false=非空 */ - private File getCurrentAppApkFile(Context context) { - try { - 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; + private boolean isStringEmpty(String str) { + return str == null || str.trim().isEmpty(); } /** * URL编码工具(Java7适配,UTF-8编码,处理特殊字符) * @param content 待编码内容 - * @return 编码后的字符串,失败返回原内容 + * @return 编码后的字符串,编码失败返回原内容 */ - private static String urlEncode(String content) { + private String urlEncode(String content) { try { return URLEncoder.encode(content, "UTF-8"); } catch (Exception e) { - LogUtils.e(TAG, "urlEncode: 编码失败,content=" + content, e); + LogUtils.e(TAG, "urlEncode: 字符串编码失败,content=" + content, e); return content; } } /** - * 从PackageManager获取应用签名SHA1指纹(BASE64编码,快速获取) - * @param context 上下文 - * @return 签名Base64字符串,失败返回null + * 主线程执行回调(统一处理,避免外部线程切换) + * @param callback 回调接口 + * @param isValid 是否合法 + * @param message 提示信息 */ - 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 e) { - LogUtils.e(TAG, "getAppSignFingerprint: 获取包信息异常", e); - } catch (NoSuchAlgorithmException e) { - LogUtils.e(TAG, "getAppSignFingerprint: 获取SHA1算法异常", e); - } catch (Exception e) { - LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e); + private void callCallbackOnMainThread(final CheckResultCallback callback, + final boolean isValid, final String message) { + if (callback == null) { + LogUtils.w(TAG, "callCallbackOnMainThread: 回调接口为null,无需执行"); + return; + } + // 已在主线程直接执行,否则切换主线程 + if (Looper.myLooper() == Looper.getMainLooper()) { + callback.onResult(isValid, message); + } else { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + callback.onResult(isValid, message); + } + }); } - return null; } - // ===================================== 回调接口 ===================================== + // ===================================== 校验结果回调接口 ===================================== /** - * 校验结果回调接口(主线程调用) + * 应用合法性校验结果回调接口(主线程调用) */ public interface CheckResultCallback { /** - * 校验结果回调 - * @param isValid 是否合法 - * @param message 校验信息/错误信息 + * 校验结果回调方法 + * @param isValid 是否合法(true=校验通过,false=校验失败) + * @param message 校验提示信息(失败时返回错误原因,成功时返回服务端提示) */ void onResult(boolean isValid, String message); } diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java index dfc163e..9285bdb 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java @@ -19,158 +19,165 @@ import java.util.jar.JarFile; /** * @Author 豆包&ZhanGSKen * @CreateTime 2026-01-24 10:00:00 - * @LastEditTime 2026-01-24 16:45:00 - * @Describe 客户端签名工具类:与服务端APKFileUtils签名/哈希校验逻辑严格对齐,兼容Java7 + * @LastEditTime 2026-01-24 19:50:00 + * @Describe 客户端签名工具类:与服务端APKFileUtils签名/哈希校验逻辑严格对齐,纯Java7实现,直接读取APK内签名文件计算,保证校验一致性 */ public class ApkSignUtils { - // ===================================== 全局常量 ===================================== + // ===================================== 全局常量定义 ===================================== private static final String TAG = "ApkSignUtils"; + // APK内签名文件路径(兼容大小写) 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 SIGN_ALGORITHM_SHA1 = "SHA1"; - private static final String SIGN_ALGORITHM_SHA256 = "SHA-256"; - private static final int BUFFER_SIZE_4K = 4096; - private static final int BUFFER_SIZE_8K = 8192; + // 加密算法常量 + private static final String ALGORITHM_SHA1 = "SHA1"; + private static final String ALGORITHM_SHA256 = "SHA-256"; + // 缓冲区大小常量(按业务场景区分) + private static final int BUFFER_4K = 4096; + private static final int BUFFER_8K = 8192; // ===================================== 对外核心方法 ===================================== /** - * 获取与服务端对齐的签名Base64(核心方法) - * 直接读取APK内CERT.RSA原始字节 → SHA1摘要 → Base64.NO_WRAP编码,与服务端逻辑完全一致 - * @param context 上下文,用于获取当前应用APK路径 - * @return 签名Base64字符串,失败返回null + * 获取与服务端对齐的签名Base64串 + * 逻辑:读取APK内CERT.RSA原始字节 → SHA1摘要 → Base64.NO_WRAP编码,与服务端完全一致 + * @param context 上下文,用于获取当前应用APK的真实安装路径 + * @return 签名Base64字符串,任意步骤失败返回null */ public static String getApkSignAlignedWithServer(Context context) { - LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始获取服务端对齐签名"); + LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始执行服务端对齐签名计算"); + // 入参空值快速校验 if (context == null) { - LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为空,直接返回null"); + LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为null,直接返回null"); return null; } try { - // 1. 获取当前应用APK的真实安装路径 + // 1. 获取当前应用APK真实路径 ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); String apkPath = appInfo.sourceDir; - LogUtils.d(TAG, "getApkSignAlignedWithServer: 获取到当前应用APK路径=" + apkPath); + LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功获取APK路径,path=" + apkPath); if (apkPath == null || apkPath.trim().isEmpty()) { - LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取APK路径为空,获取签名失败"); + LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取到的APK路径为空,无法读取签名文件"); return null; } - // 2. 读取APK内的CERT.RSA原始字节流(兼容大小写命名) + // 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); + LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功读取CERT.RSA,字节长度=" + certRawBytes.length); - // 3. SHA1摘要计算 + Base64.NO_WRAP编码(与服务端完全对齐) - MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM_SHA1); + // 3. SHA1摘要 + Base64编码(服务端对齐核心步骤) + MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA1); byte[] signDigest = md.digest(certRawBytes); String signBase64 = Base64.encodeToString(signDigest, Base64.NO_WRAP); - LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功计算服务端对齐签名Base64,完成方法执行"); + LogUtils.d(TAG, "getApkSignAlignedWithServer: 服务端对齐签名计算完成,成功返回Base64串"); return signBase64; } catch (NoSuchAlgorithmException e) { - LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取SHA1算法实例失败,获取签名失败", e); + LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取SHA1算法实例失败", e); } catch (Exception e) { - LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取服务端对齐签名发生未知异常", e); + LogUtils.e(TAG, "getApkSignAlignedWithServer: 计算服务端对齐签名发生未知异常", e); } return null; } /** * 获取当前运行APK的SHA256哈希值 - * 读取APK文件完整字节流计算SHA256,转小写64位16进制字符串,与服务端校验逻辑一致 - * @param context 上下文,用于获取当前应用APK路径 - * @return SHA256哈希小写字符串,失败返回null + * 逻辑:读取APK完整文件字节流 → SHA256摘要 → 转小写64位16进制字符串,服务端同款校验逻辑 + * @param context 上下文,用于获取当前应用APK的真实安装路径 + * @return SHA256小写16进制字符串,任意步骤失败返回null */ public static String getApkSHA256Hash(Context context) { - LogUtils.d(TAG, "getApkSHA256Hash: 方法调用,开始获取APK SHA256哈希值"); + LogUtils.d(TAG, "getApkSHA256Hash: 方法调用,开始执行APK文件SHA256哈希计算"); + // 入参空值快速校验 if (context == null) { - LogUtils.w(TAG, "getApkSHA256Hash: 入参context为空,直接返回null"); + LogUtils.w(TAG, "getApkSHA256Hash: 入参context为null,直接返回null"); return null; } try { - // 1. 获取当前应用APK的真实安装路径 + // 1. 获取当前应用APK真实路径 ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); String apkPath = appInfo.sourceDir; - LogUtils.d(TAG, "getApkSHA256Hash: 获取到当前应用APK路径=" + apkPath); + LogUtils.d(TAG, "getApkSHA256Hash: 成功获取APK路径,path=" + apkPath); if (apkPath == null || apkPath.trim().isEmpty()) { - LogUtils.e(TAG, "getApkSHA256Hash: 获取APK路径为空,获取哈希失败"); + LogUtils.e(TAG, "getApkSHA256Hash: 获取到的APK路径为空,无法读取文件计算哈希"); return null; } // 2. 读取APK文件并计算SHA256哈希 File apkFile = new File(apkPath); - MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM_SHA256); + MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA256); FileInputStream fis = new FileInputStream(apkFile); - byte[] buffer = new byte[BUFFER_SIZE_8K]; - int len; - while ((len = fis.read(buffer)) != -1) { - md.update(buffer, 0, len); + byte[] buffer = new byte[BUFFER_8K]; + int readLen; + while ((readLen = fis.read(buffer)) != -1) { + md.update(buffer, 0, readLen); } fis.close(); + LogUtils.d(TAG, "getApkSHA256Hash: APK文件读取完成,开始转换哈希结果"); - // 3. 哈希字节转小写64位16进制字符串 + // 3. 哈希字节数组转小写64位16进制字符串 byte[] hashBytes = md.digest(); StringBuilder sb = new StringBuilder(); for (byte b : hashBytes) { sb.append(String.format("%02x", b)); } String sha256Hash = sb.toString(); - LogUtils.d(TAG, "getApkSHA256Hash: 成功计算APK SHA256哈希值,完成方法执行"); + LogUtils.d(TAG, "getApkSHA256Hash: APK SHA256哈希计算完成,成功返回结果"); return sha256Hash; } catch (NoSuchAlgorithmException e) { - LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败,获取哈希失败", e); + LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败", e); } catch (Exception e) { - LogUtils.e(TAG, "getApkSHA256Hash: 获取APK SHA256哈希发生未知异常", e); + LogUtils.e(TAG, "getApkSHA256Hash: 计算APK SHA256哈希发生未知异常", e); } return null; } // ===================================== 内部工具方法 ===================================== /** - * 读取APK内CERT.RSA文件的原始字节流(兼容大小写命名:CERT.RSA/cert.rsa) - * @param apkPath 当前应用APK的完整安装路径 - * @return CERT.RSA原始字节数组,失败返回null - * @throws Exception 流读取/APK解析异常 + * 读取APK内CERT.RSA文件的原始字节流,兼容大小写命名 + * @param apkPath APK文件的完整绝对路径 + * @return CERT.RSA原始字节数组,未找到文件返回null + * @throws Exception 流读取、APK解析相关异常向上抛出 */ private static byte[] readCertRsaRawBytes(String apkPath) throws Exception { - LogUtils.d(TAG, "readCertRsaRawBytes: 方法调用,APK路径=" + apkPath); + LogUtils.d(TAG, "readCertRsaRawBytes: 方法调用,开始读取APK内签名文件,apkPath=" + apkPath); JarFile jarFile = new JarFile(apkPath); JarEntry certEntry = null; - // 优先读取大写命名,找不到则读取小写命名 + // 优先读取大写命名,不存在则尝试小写 certEntry = jarFile.getJarEntry(CERT_RSA_UPPER); if (certEntry == null) { - LogUtils.d(TAG, "readCertRsaRawBytes: 未找到META-INF/CERT.RSA,尝试读取META-INF/cert.rsa"); + LogUtils.d(TAG, "readCertRsaRawBytes: 未找到META-INF/CERT.RSA,尝试读取小写META-INF/cert.rsa"); certEntry = jarFile.getJarEntry(CERT_RSA_LOWER); } - // 未找到有效CERT.RSA文件 + // 未找到有效签名文件,关闭流后返回null if (certEntry == null) { LogUtils.e(TAG, "readCertRsaRawBytes: APK内未找到CERT.RSA/cert.rsa签名文件"); jarFile.close(); return null; } - // 读取文件原始字节流并关闭流 + // 读取文件原始字节并关闭所有流资源 InputStream is = jarFile.getInputStream(certEntry); - byte[] bytes = readStreamToBytes(is); + byte[] certBytes = readStreamToBytes(is); is.close(); jarFile.close(); - LogUtils.d(TAG, "readCertRsaRawBytes: 成功读取CERT.RSA字节,完成方法执行"); - return bytes; + LogUtils.d(TAG, "readCertRsaRawBytes: 签名文件读取完成,字节长度=" + certBytes.length); + return certBytes; } /** - * 输入流转字节数组(与服务端工具方法逻辑完全一致,4K缓冲区) + * 输入流转字节数组,通用工具方法 + * 4K缓冲区,适配小文件读取(如CERT.RSA),保证流资源正常关闭 * @param is 待读取的输入流 - * @return 字节数组,流为null返回空字节数组 - * @throws IOException 流读取异常 + * @return 转换后的字节数组,流为null返回空字节数组 + * @throws IOException 流读取相关异常向上抛出 */ private static byte[] readStreamToBytes(InputStream is) throws IOException { if (is == null) { @@ -178,13 +185,13 @@ public class ApkSignUtils { return new byte[0]; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); - byte[] buffer = new byte[BUFFER_SIZE_4K]; - int len; - while ((len = is.read(buffer)) != -1) { - bos.write(buffer, 0, len); + byte[] buffer = new byte[BUFFER_4K]; + int readLen; + while ((readLen = is.read(buffer)) != -1) { + bos.write(buffer, 0, readLen); } byte[] result = bos.toByteArray(); - // 关闭流资源,避免泄漏 + // 关闭流资源,避免内存泄漏 is.close(); bos.close(); return result; diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/views/AboutView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/AboutView.java index 53d278b..515dc16 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/views/AboutView.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/AboutView.java @@ -12,71 +12,81 @@ 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.dialogs.AppValidationDialog; import cc.winboll.studio.libappbase.models.APPInfo; /** - * @Describe AboutView 原生实现关于页面,无第三方依赖,适配API30,抽象通用功能控件(邮件/网页跳转) * @Author 豆包&ZhanGSKen - * @Date 2026/01/11 12:23:00 - * @LastEditTime 2026/01/20 20:45:00 + * @CreateTime 2026-01-11 12:23:00 + * @LastEditTime 2026-01-24 20:50:00 + * @Describe AboutView 原生实现关于页面,无第三方依赖,适配API30;抽象通用功能控件(邮件/网页跳转),支持调试工具入口动态显隐,集成应用正版校验、调试地址配置弹窗 */ public class AboutView extends LinearLayout { - // 全局常量区(标识、回调标识) + // ===================================== 全局常量 ===================================== public static final String TAG = "AboutView"; public static final int MSG_APPUPDATE_CHECKED = 0; - // 固定链接常量 + // 固定链接/邮件常量 private static final String WINBOLL_OFFICIAL_HOME = "https://www.winboll.cc"; - // 邮件相关常量(统一封装,便于维护) private static final String EMAIL_TITLE = "联系WinBoLLStudio"; private static final String EMAIL_ADDRESS = "studio@winboll.cc"; private static final String EMAIL_TYPE = "message/rfc822"; - // 布局尺寸常量(统一管理,适配多屏幕,dp为基准单位) + // 布局尺寸常量(dp) private static final int PADDING_LARGE = 32; 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 ITEM_ICON_SIZE = 24; - // 成员属性区(按 核心依赖→业务配置→视图相关 归类排序,注释清晰) - private Context mContext; // 上下文对象,全局复用 - private APPInfo mAPPInfo; // 应用核心信息实体 - private OnRequestDevUserInfoAutofillListener mOnRequestDevUserInfoAutofillListener; // 调试信息填充监听 + // 服务器默认地址常量 + private static final String SERVER_DEBUG_HOST = "https://yun-preivew.winboll.cc"; + private static final String SERVER_RELEASE_HOST = "https://yun.winboll.cc"; - private String mszAppName = ""; // 应用名称 - private String mszAppVersionName = ""; // 应用版本号 - private String mszAppDescription = ""; // 应用描述文案 - private String mszHomePage = ""; // 应用主页/APK下载地址 - private String mszGitea = ""; // 应用Git源码地址 - 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 Context mContext; + private APPInfo mAPPInfo; + private OnRequestDevUserInfoAutofillListener mOnRequestDevUserInfoAutofillListener; - // 视图绑定 + // 应用基础信息 + 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 TextView tvAppNameVersion; private TextView tvAppDesc; private LinearLayout llFunctionContainer; + private ImageButton ibSigngetDialog; + private ImageButton ibWinBoLLHostDialog; - // 构造方法区(按 参数从少到多 排序,适配 代码创建+XML引用 场景) + // ===================================== 构造方法(按参数从少到多排序) ===================================== public AboutView(Context context) { super(context); - LogUtils.d(TAG, "AboutView(Context) 构造方法调用,代码创建视图场景"); + LogUtils.d(TAG, "AboutView(Context):代码创建视图,执行默认初始化"); this.mContext = context; initDefaultParams(); initViewFromXml(); @@ -84,7 +94,7 @@ public class AboutView extends LinearLayout { public AboutView(Context context, APPInfo appInfo) { 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.mAPPInfo = appInfo; initViewFromXml(); @@ -93,7 +103,7 @@ public class AboutView extends LinearLayout { public AboutView(Context context, AttributeSet attrs) { super(context, attrs); - LogUtils.d(TAG, "AboutView(Context,AttributeSet) 构造调用,XML布局引用场景"); + LogUtils.d(TAG, "AboutView(Context,AttributeSet):XML布局引用,执行默认初始化"); this.mContext = context; initDefaultParams(); initViewFromXml(); @@ -101,70 +111,22 @@ public class AboutView extends LinearLayout { public AboutView(Context context, AttributeSet attrs, int 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; 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, mszAppGitName, mszAppVersionName).show(); // 弹出对话框 - } - }); - ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "签名获取按钮点击,弹出SignGetDialog"); - new DebugHostDialog(mContext).show(); // 弹出对话框 - } - }); - } - - - // 对外公开方法区(供外部调用,职责单一,注释明确) + // ===================================== 对外公开方法 ===================================== /** - * 一站式初始化所有关于页逻辑,包含参数、信息、视图全流程初始化 + * 一站式初始化所有关于页逻辑,包含参数、应用信息、页面视图全流程 */ public void initAll() { - LogUtils.d(TAG, "initAll() 一站式初始化调用,APPInfo是否为空:" + (mAPPInfo == null)); + LogUtils.d(TAG, "initAll():开始一站式初始化,APPInfo是否为空=" + (mAPPInfo == null)); if (mAPPInfo == null) { - LogUtils.w(TAG, "initAll() 初始化终止:APPInfo 为 null,无法获取应用核心信息"); + LogUtils.w(TAG, "initAll():初始化终止,APPInfo为null"); return; } - - // 按初始化流程执行,有序无冗余 initDefaultParams(); initAppBaseInfo(); initAppVersionInfo(); @@ -172,60 +134,81 @@ public class AboutView extends LinearLayout { initAppLinkInfo(); initReleaseAPKInfo(); initAboutPageView(); - LogUtils.d(TAG, "initAll() 所有初始化流程执行完成"); + LogUtils.d(TAG, "initAll():所有初始化流程执行完成"); } /** - * 重置应用信息并重新初始化关于页,支持动态更新页面内容 + * 重置应用信息并重新初始化页面,支持动态更新关于页内容 * @param 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; - llFunctionContainer.removeAllViews(); + if (llFunctionContainer != null) llFunctionContainer.removeAllViews(); initAll(); - LogUtils.d(TAG, "setAPPInfoAndInit() 应用信息重置+页面重构完成"); + LogUtils.d(TAG, "setAPPInfoAndInit():应用信息重置+页面重构完成"); } /** - * 设置应用信息(兼容旧调用逻辑),设置后自动重构页面 + * 设置应用信息,兼容旧调用逻辑,设置后自动重构页面 * @param 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; - llFunctionContainer.removeAllViews(); + if (llFunctionContainer != null) llFunctionContainer.removeAllViews(); initAll(); } /** - * 设置调试信息自动填充监听,用于调试场景的信息回调 + * 设置调试信息自动填充监听,供调试场景回调使用 * @param l 监听回调接口实现 */ public void setOnRequestDevUserInfoAutofillListener(OnRequestDevUserInfoAutofillListener l) { - LogUtils.d(TAG, "setOnRequestDevUserInfoAutofillListener() 调试监听设置完成"); + LogUtils.d(TAG, "setOnRequestDevUserInfoAutofillListener():设置调试信息填充监听完成"); this.mOnRequestDevUserInfoAutofillListener = l; } - // 内部初始化方法区(按 基础→业务→视图 流程排序,单一职责) + // ===================================== 内部初始化方法 ===================================== /** * 初始化默认兜底参数,防止空指针,为后续初始化做基础铺垫 */ private void initDefaultParams() { - LogUtils.d(TAG, "initDefaultParams() 执行默认参数初始化"); - mszWinBoLLServerHost = GlobalApplication.isDebugging() ? "https://yun-preivew.winboll.cc" : "https://yun.winboll.cc"; - mnAppIcon = mnAppIcon == 0 ? R.drawable.ic_winboll : mnAppIcon; + LogUtils.d(TAG, "initDefaultParams():开始初始化默认参数"); + mszWinBoLLServerHost = GlobalApplication.isDebugging() ? SERVER_DEBUG_HOST : SERVER_RELEASE_HOST; + mnAppIcon = (mnAppIcon == 0) ? R.drawable.ic_winboll : mnAppIcon; 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实体读取应用基础核心配置,赋值到本地属性 */ private void initAppBaseInfo() { - LogUtils.d(TAG, "initAppBaseInfo() 读取APPInfo基础配置"); + LogUtils.d(TAG, "initAppBaseInfo():开始读取APPInfo基础配置"); if (mAPPInfo == null) { - LogUtils.w(TAG, "initAppBaseInfo() 跳过执行:APPInfo 为 null"); + LogUtils.w(TAG, "initAppBaseInfo():跳过执行,APPInfo为null"); return; } mszAppName = mAPPInfo.getAppName() == null ? "" : mAPPInfo.getAppName(); @@ -233,44 +216,44 @@ public class AboutView extends LinearLayout { mszAppAPKName = mAPPInfo.getAppAPKName() == null ? "" : mAPPInfo.getAppAPKName(); mszAppGitName = mAPPInfo.getAppGitName() == null ? "" : mAPPInfo.getAppGitName(); mszAppDescription = mAPPInfo.getAppDescription() == null ? "" : mAPPInfo.getAppDescription(); - mnAppIcon = mAPPInfo.getAppIcon() != 0 ? mAPPInfo.getAppIcon() : mnAppIcon; + mnAppIcon = (mAPPInfo.getAppIcon() != 0) ? mAPPInfo.getAppIcon() : mnAppIcon; mIsAddDebugTools = mAPPInfo.isAddDebugTools(); - LogUtils.d(TAG, "initAppBaseInfo() 读取完成,应用名:" + mszAppName + ",调试开关:" + mIsAddDebugTools); + LogUtils.d(TAG, "initAppBaseInfo():基础配置读取完成,应用名=" + mszAppName + ",调试开关=" + mIsAddDebugTools); } /** - * 初始化应用版本信息,从包管理中获取当前应用版本号 + * 从包管理中获取当前应用版本号,初始化版本相关信息 */ private void initAppVersionInfo() { - LogUtils.d(TAG, "initAppVersionInfo() 初始化应用版本信息"); + LogUtils.d(TAG, "initAppVersionInfo():开始初始化应用版本信息"); try { mszAppVersionName = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionName; } catch (PackageManager.NameNotFoundException e) { - LogUtils.d(TAG, "initAppVersionInfo() 获取版本号失败,默认赋值unknown", e); + LogUtils.e(TAG, "initAppVersionInfo():获取版本号失败,默认赋值unknown", e); mszAppVersionName = "unknown"; } 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() { - LogUtils.d(TAG, "initServerConfig() 服务器配置初始化(预留扩展)"); + LogUtils.d(TAG, "initServerConfig():服务器配置初始化,预留扩展接口"); } /** - * 初始化应用相关链接(主页+Git源码地址),动态拼接Git地址 + * 初始化应用相关链接(主页+Git源码地址),根据分支配置动态拼接Git地址 */ private void initAppLinkInfo() { - LogUtils.d(TAG, "initAppLinkInfo() 初始化应用链接信息"); + LogUtils.d(TAG, "initAppLinkInfo():开始初始化应用链接信息"); if (mAPPInfo == null) { - LogUtils.w(TAG, "initAppLinkInfo() 跳过执行:APPInfo 为 null"); + LogUtils.w(TAG, "initAppLinkInfo():跳过执行,APPInfo为null"); return; } mszHomePage = mAPPInfo.getAppHomePage() == null ? "" : mAPPInfo.getAppHomePage(); - // 分场景拼接Git地址,兼容无分支配置场景 + // 拼接Git地址,兼容无分支配置场景 if (mAPPInfo.getAppGitAPPBranch() == null || mAPPInfo.getAppGitAPPBranch().trim().isEmpty()) { mszGitea = String.format("https://gitea.winboll.cc/%s/%s", mAPPInfo.getAppGitOwner(), mszAppGitName); } else { @@ -278,31 +261,31 @@ public class AboutView extends LinearLayout { mAPPInfo.getAppGitOwner(), mszAppGitName, mAPPInfo.getAppGitAPPBranch(), mAPPInfo.getAppGitAPPSubProjectFolder()); } - LogUtils.d(TAG, "initAppLinkInfo() 完成,应用主页:" + mszHomePage + ",Git地址:" + mszGitea); + LogUtils.d(TAG, "initAppLinkInfo():链接信息初始化完成,应用主页=" + mszHomePage + ",Git地址=" + mszGitea); } /** * 初始化正式版APK信息,去除beta后缀适配正式包命名规范 */ private void initReleaseAPKInfo() { - LogUtils.d(TAG, "initReleaseAPKInfo() 初始化正式版APK信息"); + LogUtils.d(TAG, "initReleaseAPKInfo():开始初始化正式版APK信息"); String szReleaseAppVersionName = "unknown"; try { String szSubBetaSuffix = subBetaSuffix(mContext.getPackageName()); szReleaseAppVersionName = mContext.getPackageManager().getPackageInfo(szSubBetaSuffix, 0).versionName; } catch (PackageManager.NameNotFoundException e) { - LogUtils.d(TAG, "initReleaseAPKInfo() 获取正式版版本号失败", e); + LogUtils.e(TAG, "initReleaseAPKInfo():获取正式版版本号失败", e); } mszReleaseAPKName = String.format("%s_%s.apk", mszAppAPKName, szReleaseAppVersionName); - LogUtils.d(TAG, "initReleaseAPKInfo() 完成,正式版APK名:" + mszReleaseAPKName); + LogUtils.d(TAG, "initReleaseAPKInfo():正式版APK信息初始化完成,APK名=" + mszReleaseAPKName); } /** - * 核心视图组装:赋值基础信息+添加功能项 + * 核心视图组装:赋值基础信息到控件,添加通用功能项到容器 */ private void initAboutPageView() { - LogUtils.d(TAG, "initAboutPageView() 开始组装关于页视图"); - // 基础信息赋值 + LogUtils.d(TAG, "initAboutPageView():开始组装关于页视图"); + // 赋值基础信息 ivAppIcon.setImageResource(mnAppIcon); tvAppNameVersion.setText(String.format("%s %s", mszAppName, mszAppVersionName)); if (mszAppDescription.isEmpty()) { @@ -311,8 +294,7 @@ public class AboutView extends LinearLayout { tvAppDesc.setVisibility(VISIBLE); tvAppDesc.setText(mszAppDescription); } - - // 通用功能控件:网页跳转类+邮件类,复用抽象控件 + // 添加通用功能项 addFunctionView(new WebJumpFunctionItemView(mContext, "WinBoLL 主页", WINBOLL_OFFICIAL_HOME, R.drawable.ic_winboll)); addFunctionView(new EmailFunctionItemView(mContext, "联系邮箱", "WinBoLLStudio", R.drawable.ic_winboll)); if (!mszHomePage.isEmpty()) { @@ -321,19 +303,46 @@ public class AboutView extends LinearLayout { if (!mszGitea.isEmpty()) { 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) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); params.topMargin = dp2px(PADDING_SMALL); llFunctionContainer.addView(view, params); } - // 工具方法区(通用工具+业务工具,静态优先,便于复用) /** - * dp 转 px 工具方法,适配不同屏幕密度,保证布局一致性 + * dp转px工具方法,适配不同屏幕密度,保证布局一致性 * @param dpValue dp单位尺寸 * @return 转换后的px单位尺寸 */ @@ -348,17 +357,20 @@ public class AboutView extends LinearLayout { * @return 去除beta后缀后的正式包名 */ public static String subBetaSuffix(String input) { - LogUtils.d(TAG, "subBetaSuffix() 执行包名beta后缀去除,原始包名:" + input); + LogUtils.d(TAG, "subBetaSuffix():执行包名beta后缀去除,原始包名=" + input); if (input != null && input.endsWith(".beta")) { String result = input.substring(0, input.length() - ".beta".length()); - LogUtils.d(TAG, "subBetaSuffix() 处理成功,正式包名:" + result); + LogUtils.d(TAG, "subBetaSuffix():处理成功,正式包名=" + result); return result; } - LogUtils.d(TAG, "subBetaSuffix() 无需处理,包名不含beta后缀"); + LogUtils.d(TAG, "subBetaSuffix():无需处理,包名不含beta后缀"); return input == null ? "" : input; } - // 内部抽象通用功能项基类 - 统一样式,减少冗余 + // ===================================== 内部抽象通用功能项基类 ===================================== + /** + * 通用功能项基类,统一样式、布局、视图构建,减少冗余代码 + */ private abstract class BaseFunctionItemView extends LinearLayout implements OnClickListener { protected Context mItemContext; protected String mTitle; @@ -376,7 +388,9 @@ public class AboutView extends LinearLayout { setOnClickListener(this); } - // 统一布局配置 + /** + * 统一初始化功能项布局属性 + */ private void initItemLayout() { setOrientation(HORIZONTAL); setGravity(Gravity.CENTER_VERTICAL); @@ -386,7 +400,9 @@ public class AboutView extends LinearLayout { setBackgroundResource(android.R.drawable.list_selector_background); } - // 统一视图构建 + /** + * 统一构建功能项视图(左侧图标+右侧标题/内容) + */ private void initItemViews() { // 左侧图标 if (mIconRes != 0) { @@ -397,20 +413,17 @@ public class AboutView extends LinearLayout { ivIcon.setImageResource(mIconRes); addView(ivIcon); } - // 右侧文本容器 LinearLayout llText = new LinearLayout(mItemContext); llText.setOrientation(VERTICAL); llText.setLayoutParams(new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1.0f)); addView(llText); - // 标题 TextView tvTitle = new TextView(mItemContext); tvTitle.setText(mTitle); tvTitle.setTextSize(16); tvTitle.setTextColor(mItemContext.getResources().getColor(R.color.gray_900)); llText.addView(tvTitle); - // 内容 TextView tvContent = new TextView(mItemContext); tvContent.setText(mContent); @@ -420,11 +433,17 @@ public class AboutView extends LinearLayout { llText.addView(tvContent); } - // 子类指定内容文本颜色 + /** + * 子类抽象方法:指定内容文本颜色 + * @return 颜色值 + */ protected abstract int getContentTextColor(); } - // 邮件类功能控件 - 专属邮件唤起逻辑 + // ===================================== 内部邮件功能项子类 ===================================== + /** + * 邮件类功能控件,实现专属邮件唤起逻辑,双方案兼容(纯邮件客户端/通用邮件应用) + */ private class EmailFunctionItemView extends BaseFunctionItemView { public EmailFunctionItemView(Context context, String title, String content, int iconRes) { super(context, title, content, iconRes); @@ -437,36 +456,37 @@ public class AboutView extends LinearLayout { @Override public void onClick(View v) { - LogUtils.d(TAG, "EmailFunctionItemView onClick 触发邮件唤起"); - // 双方案邮件唤起逻辑 + LogUtils.d(TAG, "EmailFunctionItemView onClick:触发邮件唤起逻辑"); + // 方案1:纯邮件客户端唤起 Intent emailIntent = new Intent(Intent.ACTION_SENDTO); emailIntent.setData(Uri.parse("mailto:" + EMAIL_ADDRESS)); emailIntent.putExtra(Intent.EXTRA_SUBJECT, EMAIL_TITLE); emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (emailIntent.resolveActivity(mItemContext.getPackageManager()) != null) { mItemContext.startActivity(emailIntent); - LogUtils.d(TAG, "邮件唤起成功:系统纯邮件客户端"); + LogUtils.d(TAG, "EmailFunctionItemView:纯邮件客户端唤起成功"); return; } - + // 方案2:通用邮件应用兜底 Intent fallbackIntent = new Intent(Intent.ACTION_SEND); fallbackIntent.setType(EMAIL_TYPE); fallbackIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{EMAIL_ADDRESS}); fallbackIntent.putExtra(Intent.EXTRA_SUBJECT, EMAIL_TITLE); fallbackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (fallbackIntent.resolveActivity(mItemContext.getPackageManager()) != null) { mItemContext.startActivity(fallbackIntent); - LogUtils.d(TAG, "邮件唤起成功:通用邮件应用"); + LogUtils.d(TAG, "EmailFunctionItemView:通用邮件应用唤起成功"); } else { ToastUtils.show("未找到可发送邮件的应用"); - LogUtils.w(TAG, "邮件唤起失败:无可用邮件相关应用"); + LogUtils.w(TAG, "EmailFunctionItemView:邮件唤起失败,无可用邮件应用"); } } } - // 网页跳转类功能控件 - 专属网页跳转逻辑 + // ===================================== 内部网页跳转功能项子类 ===================================== + /** + * 网页跳转类功能控件,实现专属网页唤起逻辑,包含空地址校验、异常捕获 + */ private class WebJumpFunctionItemView extends BaseFunctionItemView { public WebJumpFunctionItemView(Context context, String title, String content, int iconRes) { super(context, title, content, iconRes); @@ -479,25 +499,28 @@ public class AboutView extends LinearLayout { @Override public void onClick(View v) { - LogUtils.d(TAG, "WebJumpFunctionItemView onClick 触发网页跳转,地址:" + mContent); + LogUtils.d(TAG, "WebJumpFunctionItemView onClick:触发网页跳转,地址=" + mContent); if (mContent.isEmpty()) { ToastUtils.show("跳转地址为空"); - LogUtils.w(TAG, "网页跳转失败:地址为空"); + LogUtils.w(TAG, "WebJumpFunctionItemView:网页跳转失败,地址为空"); return; } try { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(mContent)); browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mItemContext.startActivity(browserIntent); - LogUtils.d(TAG, "网页跳转成功"); + LogUtils.d(TAG, "WebJumpFunctionItemView:网页跳转成功"); } catch (Exception e) { - LogUtils.d(TAG, "网页跳转失败,异常捕获", e); + LogUtils.e(TAG, "WebJumpFunctionItemView:网页跳转失败", e); ToastUtils.show("链接无法打开"); } } } - // 内部接口区(置于类末尾,逻辑闭环) + // ===================================== 内部回调接口 ===================================== + /** + * 调试信息自动填充回调接口 + */ public interface OnRequestDevUserInfoAutofillListener { void requestAutofill(EditText etDevUserName, EditText etDevUserPassword); }