diff --git a/appbase/build.properties b/appbase/build.properties index ab14b05..81e2d66 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat May 16 02:12:52 CST 2026 +#Fri May 15 18:56:10 GMT 2026 stageCount=10 libraryProject=libappbase baseVersion=15.20 publishVersion=15.20.9 -buildCount=1 +buildCount=4 baseBetaVersion=15.20.10 diff --git a/libappbase/build.gradle b/libappbase/build.gradle index be09ef5..876e8b7 100644 --- a/libappbase/build.gradle +++ b/libappbase/build.gradle @@ -26,11 +26,4 @@ android { dependencies { api fileTree(dir: 'libs', include: ['*.jar']) - - // JSch for SFTP - api 'com.jcraft:jsch:0.1.55' - // Gson for JSON - api 'com.google.code.gson:gson:2.8.9' - // OkHttp for HTTP - api 'com.squareup.okhttp3:okhttp:3.12.13' } diff --git a/libappbase/build.properties b/libappbase/build.properties index ab14b05..81e2d66 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat May 16 02:12:52 CST 2026 +#Fri May 15 18:56:10 GMT 2026 stageCount=10 libraryProject=libappbase baseVersion=15.20 publishVersion=15.20.9 -buildCount=1 +buildCount=4 baseBetaVersion=15.20.10 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 deleted file mode 100644 index 2e70ea8..0000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/APPValidationDialog.java +++ /dev/null @@ -1,186 +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; - -/** - * @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 appName; - private String versionName; - private String clientSign; - private String clientHash; - - // ===================================== 构造方法 ===================================== - public APPValidationDialog(Context context, String appName, String versionName) { - super(context, R.style.DialogStyle); - this.mContext = context; - this.appName = appName; - this.versionName = versionName; - LogUtils.d(TAG, "AppValidationDialog: 构造方法初始化,入参-> projectName=" + appName + ", 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, - appName, - 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/utils/APPUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/APPUtils.java deleted file mode 100644 index b815497..0000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/APPUtils.java +++ /dev/null @@ -1,199 +0,0 @@ -package cc.winboll.studio.libappbase.utils; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.util.Base64; - -import cc.winboll.studio.libappbase.GlobalApplication; -import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.libappbase.models.SignCheckResponse; - -import com.google.gson.Gson; - -import java.io.IOException; -import java.net.URLEncoder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -/** - * @Author 豆包&ZhanGSKen - * @CreateTime 2026-01-20 19:17:00 - * @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 final OkHttpClient sOkHttpClient = new OkHttpClient(); - // Gson解析单例(全局复用,提高解析效率) - private static final Gson sGson = new Gson(); - - // ===================================== 对外核心校验方法 ===================================== - /** - * 检查应用合法性(外部传入签名+哈希,拼接调试标识发起网络校验) - * @param context 上下文,用于主线程回调 - * @param projectName 项目名称(服务端区分项目标识) - * @param versionName 应用版本名(服务端版本校验) - * @param clientSign 外部计算的应用签名字符串(Base64) - * @param clientHash 外部计算的APK SHA256哈希字符串(小写16进制) - * @param callback 校验结果回调(主线程调用,返回是否合法+提示信息) - */ - public void checkAPKValidation(Context context, String appName, String versionName, - String clientSign, String clientHash, final CheckResultCallback callback) { - // 方法调用+全量入参调试日志 - LogUtils.d(TAG, "checkAPKValidation: 方法调用,入参-> appName=" + appName - + ", versionName=" + versionName + ", clientSign=" + clientSign + ", clientHash=" + clientHash); - - // 1. 核心入参空值校验(快速失败) - if (context == null) { - LogUtils.w(TAG, "checkAPKValidation: 入参context为空,直接返回校验失败"); - callCallbackOnMainThread(callback, false, "上下文对象不能为空"); - return; - } - if (isStringEmpty(appName)) { - LogUtils.w(TAG, "checkAPKValidation: 入参projectName为空/空白,直接返回校验失败"); - callCallbackOnMainThread(callback, false, "项目名称不能为空"); - return; - } - 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: 入参校验通过,开始处理网络请求"); - - // 2. 动态参数URL编码(避免特殊字符导致请求解析异常) - LogUtils.d(TAG, "checkAPKValidation: 开始对动态参数进行UTF-8 URL编码"); - String encodeProjectName = urlEncode(appName); - String encodeVersionName = urlEncode(versionName); - String encodeClientSign = urlEncode(clientSign); - String encodeClientHash = urlEncode(clientHash); - String isDebug = String.valueOf(GlobalApplication.isDebugging()); - LogUtils.d(TAG, "checkAPKValidation: 参数编码完成,debug标识=" + isDebug); - - // 3. 构建完整网络校验请求URL - String requestUrl = String.format("%s?isDebug=%s&projectName=%s&versionName=%s&clientSign=%s&clientHash=%s", - GlobalApplication.getWinbollHost() + CHECK_API_URI, - isDebug, - encodeProjectName, - encodeVersionName, - encodeClientSign, - encodeClientHash); - LogUtils.d(TAG, "checkAPKValidation: 构建网络校验请求URL=" + requestUrl); - - // 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) { - String errorMsg = "网络校验请求失败:" + e.getMessage(); - LogUtils.e(TAG, "checkAPKValidation: " + errorMsg, e); - 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); - 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 { - // 响应失败,返回状态码信息 - String errorMsg = "网络校验响应失败,服务端状态码=" + response.code(); - LogUtils.e(TAG, "checkAPKValidation: " + errorMsg); - callCallbackOnMainThread(callback, false, errorMsg); - } - } - }); - } - - // ===================================== 内部工具方法 ===================================== - /** - * 字符串空值/空白校验工具 - * @param str 待校验字符串 - * @return true=空/空白,false=非空 - */ - private boolean isStringEmpty(String str) { - return str == null || str.trim().isEmpty(); - } - - /** - * URL编码工具(Java7适配,UTF-8编码,处理特殊字符) - * @param content 待编码内容 - * @return 编码后的字符串,编码失败返回原内容 - */ - private String urlEncode(String content) { - try { - return URLEncoder.encode(content, "UTF-8"); - } catch (Exception e) { - LogUtils.e(TAG, "urlEncode: 字符串编码失败,content=" + content, e); - return content; - } - } - - /** - * 主线程执行回调(统一处理,避免外部线程切换) - * @param callback 回调接口 - * @param isValid 是否合法 - * @param message 提示信息 - */ - 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); - } - }); - } - } - - // ===================================== 校验结果回调接口 ===================================== - /** - * 应用合法性校验结果回调接口(主线程调用) - */ - public interface CheckResultCallback { - /** - * 校验结果回调方法 - * @param isValid 是否合法(true=校验通过,false=校验失败) - * @param message 校验提示信息(失败时返回错误原因,成功时返回服务端提示) - */ - void onResult(boolean isValid, String message); - } -} - diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/BackupUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/BackupUtils.java deleted file mode 100644 index dcbe80a..0000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/BackupUtils.java +++ /dev/null @@ -1,305 +0,0 @@ -package cc.winboll.studio.libappbase.utils; - -import android.content.Context; -import android.os.Environment; -import android.text.TextUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.libappbase.models.SFTPAuthModel; - -/** - * 文件备份工具类(单例模式) - * 区分应用Data目录/应用专属外部文件目录双Map管理备份文件路径 - * 核心功能:文件添加/移除 + ZIP打包(分data/sdcard目录) + SFTP分步式上传(登录→传输→登出) - * 依赖:FTPUtils(单例)、SFTPAuthModel(外部实体类)、Android上下文 - * 兼容:Java7、Android 6.0+,无第三方依赖(ZIP为原生实现),免动态读写权限 - * @Author 豆包&ZhanGSKen - * @Date 2026/01/30 20:18:00 - * @LastEditTime 2026/02/01 02:05:00 - */ -public class BackupUtils { - public static final String TAG = "BackupUtils"; - // ZIP内部分级目录常量(统一维护,便于修改) - private static final String ZIP_DIR_DATA = "data/"; - private static final String ZIP_DIR_SDCARD = "sdcard/"; - - // 单例实例(双重校验锁,volatile保证可见性,线程安全) - private static volatile BackupUtils sInstance; - - // 双Map分目录管理:key=文件唯一标识,value=对应目录下的相对路径 - private final Map mDataDirFileMap; // 基础根目录:应用私有Data目录(/data/data/[包名]/files) - private final Map mSdcardFileMap; // 基础根目录:应用专属外部文件目录(/storage/emulated/0/Android/data/[包名]/files) - - // 全局上下文(持有Application上下文,避免Activity内存泄漏) - private Context mAppContext; - // SFTP认证配置(直接引用外部实体类,无内部封装) - private SFTPAuthModel mFtpAuthModel; - // SFTP服务器指定上传目录(独立参数传入,标准化后作为成员变量) - private String mFtpTargetDir; - // 应用专属外部文件目录(SDCard Map的基础根目录,初始化时赋值,避免重复创建) - private File mAppExternalFilesDir; - - // 私有构造器:新增双Map入参,空值则使用内部默认初始化,非空则用入参初始化 - private BackupUtils(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir, - Map dataDirFileMap, Map sdcardFileMap) { - this.mAppContext = context.getApplicationContext(); - this.mFtpAuthModel = ftpAuthModel; - // 初始化SDCard Map的基础根目录:应用专属外部文件目录(/storage/emulated/0/Android/data/[包名]/files) - this.mAppExternalFilesDir = mAppContext.getExternalFilesDir(null); - // 标准化SFTP上传目录:空则默认/,非空则补全结尾斜杠 - this.mFtpTargetDir = TextUtils.isEmpty(ftpTargetDir) ? "/" : (ftpTargetDir.endsWith("/") ? ftpTargetDir : ftpTargetDir + "/"); - - // 核心修改:入参Map非空且非空集合时,使用入参初始化;否则内部new HashMap() - this.mDataDirFileMap = (dataDirFileMap != null && !dataDirFileMap.isEmpty()) - ? new HashMap(dataDirFileMap) - : new HashMap(); - this.mSdcardFileMap = (sdcardFileMap != null && !sdcardFileMap.isEmpty()) - ? new HashMap(sdcardFileMap) - : new HashMap(); - - LogUtils.d(TAG, "BackupUtils初始化完成 → SFTP服务器:" + ftpAuthModel.getFtpServer() + ":" + ftpAuthModel.getFtpPort() + " | 上传目录:" + mFtpTargetDir); - LogUtils.d(TAG, "SDCard Map基础根目录:" + (mAppExternalFilesDir == null ? "获取失败" : mAppExternalFilesDir.getAbsolutePath())); - LogUtils.d(TAG, "初始化后DataMap大小:" + mDataDirFileMap.size() + " | SdcardMap大小:" + mSdcardFileMap.size()); - } - - /** - * 单例初始化方法(必须先调用,否则getInstance()会抛异常) - * 新增双Map入参,支持外部初始化待备份文件列表 - * @param context 上下文(推荐传Application,避免内存泄漏) - * @param ftpAuthModel 外部SFTP认证实体类(含服务器/账号/端口等) - * @param ftpTargetDir SFTP服务器指定上传目录(如/backup,自动补全斜杠) - * @param dataDirFileMap 外部传入的Data目录文件Map,null/空则内部默认初始化 - * @param sdcardFileMap 外部传入的SDCard目录文件Map,null/空则内部默认初始化 - * @return BackupUtils单例实例 - */ - public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir, - Map dataDirFileMap, Map sdcardFileMap) { - if (sInstance == null) { - synchronized (BackupUtils.class) { - if (sInstance == null) { - // 前置强校验:避免空参数导致后续空指针 - if (context == null) { - throw new IllegalArgumentException("初始化失败:Context 不能为空"); - } - if (ftpAuthModel == null || TextUtils.isEmpty(ftpAuthModel.getFtpServer())) { - throw new IllegalArgumentException("初始化失败:SFTPAuthModel/ftpServer 不能为空"); - } - // 透传新增的双Map入参至构造器 - sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir, dataDirFileMap, sdcardFileMap); - } - } - } - return sInstance; - } - - /** - * 重载默认初始化方法:兼容原有调用逻辑,无需传入Map,内部默认初始化 - * 避免修改后影响原有代码调用 - */ - public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) { - return getInstance(context, ftpAuthModel, ftpTargetDir, null, null); - } - - /** - * 获取单例实例(需先调用带参getInstance初始化) - * @return BackupUtils单例实例 - */ - public static BackupUtils getInstance() { - if (sInstance == null) { - throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, SFTPAuthModel, String[, Map, Map])"); - } - return sInstance; - } - - // ====================================== 以下原有方法均未修改 ====================================== - public void addDataDirFile(String key, String relativePath) { - if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath)) { - mDataDirFileMap.put(key, relativePath); - LogUtils.d(TAG, "添加Data目录文件:" + key + " → " + relativePath); - } - } - - public void removeDataDirFile(String key) { - if (!TextUtils.isEmpty(key) && mDataDirFileMap.containsKey(key)) { - mDataDirFileMap.remove(key); - LogUtils.d(TAG, "移除Data目录文件:" + key); - } - } - - public String getDataDirFile(String key) { - return mDataDirFileMap.get(key); - } - - public Map getAllDataDirFiles() { - return new HashMap<>(mDataDirFileMap); - } - - public void clearDataDirFiles() { - mDataDirFileMap.clear(); - LogUtils.d(TAG, "清空Data目录所有备份文件"); - } - - public void addSdcardFile(String key, String relativePath) { - if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath) && mAppExternalFilesDir != null) { - mSdcardFileMap.put(key, relativePath); - LogUtils.d(TAG, "添加外部文件目录文件:" + key + " → " + relativePath); - } - } - - public void removeSdcardFile(String key) { - if (!TextUtils.isEmpty(key) && mSdcardFileMap.containsKey(key)) { - mSdcardFileMap.remove(key); - LogUtils.d(TAG, "移除外部文件目录文件:" + key); - } - } - - public String getSdcardFile(String key) { - return mSdcardFileMap.get(key); - } - - public Map getAllSdcardFiles() { - return new HashMap<>(mSdcardFileMap); - } - - public void clearSdcardFiles() { - mSdcardFileMap.clear(); - LogUtils.d(TAG, "清空外部文件目录所有备份文件"); - } - - public boolean packAndUploadByFtp() { - if (mDataDirFileMap.isEmpty() && mSdcardFileMap.isEmpty()) { - LogUtils.e(TAG, "SFTP上传失败:无待备份文件(DataDir+外部文件目录均为空)"); - return false; - } - if (mAppExternalFilesDir == null) { - LogUtils.e(TAG, "SFTP上传失败:应用专属外部文件目录获取失败,无法访问文件"); - return false; - } - - String zipFileName = UUID.randomUUID().toString().replace("-", "") - + "-" + System.currentTimeMillis() + ".zip"; - File tempZipFile = new File(mAppContext.getExternalCacheDir(), zipFileName); - String remoteFtpFilePath = mFtpTargetDir + zipFileName; - - FTPUtils ftpUtils = FTPUtils.getInstance(); - boolean isUploadSuccess = false; - - try { - LogUtils.d(TAG, "开始SFTP登录:" + mFtpAuthModel.getFtpServer() + ":" + mFtpAuthModel.getFtpPort()); - boolean isFtpLogin = ftpUtils.login(mFtpAuthModel); - if (!isFtpLogin) { - LogUtils.e(TAG, "SFTP上传失败:SFTP登录失败(账号/密码/服务器/端口错误)"); - return false; - } - LogUtils.i(TAG, "SFTP登录成功,准备打包文件:" + zipFileName); - - LogUtils.d(TAG, "开始本地ZIP打包(分data/sdcard目录),临时文件路径:" + tempZipFile.getAbsolutePath()); - boolean isPackSuccess = packFilesToZip(tempZipFile); - if (!isPackSuccess || !tempZipFile.exists() || tempZipFile.length() == 0) { - LogUtils.e(TAG, "SFTP上传失败:ZIP打包失败(文件不存在/空文件)"); - return false; - } - LogUtils.i(TAG, "ZIP打包成功,文件大小:" + tempZipFile.length() / 1024 + "KB"); - - LogUtils.d(TAG, "开始SFTP上传:本地→SFTP" + remoteFtpFilePath); - isUploadSuccess = ftpUtils.uploadFile(tempZipFile.getAbsolutePath(), remoteFtpFilePath); - if (isUploadSuccess) { - LogUtils.i(TAG, "SFTP上传全流程成功:" + remoteFtpFilePath); - } else { - LogUtils.e(TAG, "SFTP上传失败:文件传输到服务器失败(响应码异常/权限不足)"); - } - - } catch (Exception e) { - LogUtils.e(TAG, "SFTP上传异常:" + e.getMessage(), e); - isUploadSuccess = false; - } finally { - if (ftpUtils.isConnected()) { - ftpUtils.logout(); - } - ftpUtils.disconnect(); - if (tempZipFile.exists()) { - boolean isDelete = tempZipFile.delete(); - LogUtils.d(TAG, "本地临时ZIP文件删除:" + (isDelete ? "成功" : "失败")); - } - System.gc(); - } - - return isUploadSuccess; - } - - private boolean packFilesToZip(File zipFile) { - ZipOutputStream zos = null; - try { - zos = new ZipOutputStream(new FileOutputStream(zipFile), Charset.forName("UTF-8")); - zos.setLevel(ZipOutputStream.DEFLATED); - - if (!mDataDirFileMap.isEmpty()) { - packDirFilesToZip(zos, mDataDirFileMap, mAppContext.getFilesDir(), ZIP_DIR_DATA); - LogUtils.d(TAG, "Data目录文件已打包到ZIP→" + ZIP_DIR_DATA + "子目录"); - } - if (!mSdcardFileMap.isEmpty() && mAppExternalFilesDir != null) { - packDirFilesToZip(zos, mSdcardFileMap, mAppExternalFilesDir, ZIP_DIR_SDCARD); - LogUtils.d(TAG, "应用专属外部文件目录文件已打包到ZIP→" + ZIP_DIR_SDCARD + "子目录"); - } - - zos.flush(); - return true; - } catch (IOException e) { - LogUtils.e(TAG, "ZIP打包IO异常:" + e.getMessage(), e); - return false; - } finally { - if (zos != null) { - try { - zos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "关闭ZIP流异常:" + e.getMessage(), e); - } - } - } - } - - private void packDirFilesToZip(ZipOutputStream zos, Map fileMap, File baseDir, String zipSubDir) { - for (Map.Entry entry : fileMap.entrySet()) { - String relativePath = entry.getValue(); - if (TextUtils.isEmpty(relativePath)) { - continue; - } - File localFile = new File(baseDir, relativePath); - if (!localFile.exists() || !localFile.isFile()) { - LogUtils.w(TAG, "跳过无效文件:" + localFile.getAbsolutePath()); - continue; - } - String zipInnerPath = zipSubDir + relativePath; - try { - addSingleFileToZip(zos, localFile, zipInnerPath); - } catch (IOException e) { - LogUtils.e(TAG, "打包单个文件失败:" + zipInnerPath, e); - } - } - } - - private void addSingleFileToZip(ZipOutputStream zos, File localFile, String zipInnerPath) throws IOException { - ZipEntry zipEntry = new ZipEntry(zipInnerPath); - zos.putNextEntry(zipEntry); - FileInputStream fis = new FileInputStream(localFile); - byte[] buffer = new byte[4096]; - int len; - while ((len = fis.read(buffer)) != -1) { - zos.write(buffer, 0, len); - } - fis.close(); - zos.closeEntry(); - } -} - diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FTPUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FTPUtils.java deleted file mode 100644 index 7e1ea28..0000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FTPUtils.java +++ /dev/null @@ -1,487 +0,0 @@ -package cc.winboll.studio.libappbase.utils; - -import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.libappbase.models.SFTPAuthModel; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.SftpATTRS; -import com.jcraft.jsch.SftpException; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Properties; -import java.util.Vector; - -/** - * SFTP/FTP工具类(单例模式)- Java7兼容 · 适配FTPAuthModel实体类 - * 底层严格基于JSch 0.1.54原生ChannelSftp+SftpException接口实现,替换原commons-net FTP - * 核心功能:登录/登出、文件上传/下载、文件夹列举、文件/文件夹存在性判断 - * 依赖:com.jcraft:jsch:0.1.54 - * @Author 豆包&ZhanGSKen - * @Date 2026/01/30 19:04 - */ -public class FTPUtils { - // 单例实例(双重校验锁 volatile 保证可见性,Java7兼容) - private static volatile FTPUtils sInstance; - // JSch核心对象:Session(连接会话)、ChannelSftp(SFTP通道) - private JSch mJSch; - private Session mSession; - private ChannelSftp mSftpChannel; - // 日志TAG - public static final String TAG = "FTPUtils"; - // SFTP默认端口(FTPAuthModel未设置时使用) - private static final int DEFAULT_SFTP_PORT = 22; - // 连接超时时间 5s(Java7原生Socket超时) - private static final int CONNECT_TIMEOUT = 5000; - - // 私有构造器:禁止外部实例化 - private FTPUtils() { - initSftpClient(); - } - - /** - * 获取单例实例(双重校验锁,线程安全,Java7兼容) - * @return FTPUtils 单例 - */ - public static FTPUtils getInstance() { - if (sInstance == null) { - synchronized (FTPUtils.class) { - if (sInstance == null) { - sInstance = new FTPUtils(); - } - } - } - return sInstance; - } - - /** - * 初始化SFTP客户端(JSch),创建核心原生对象 - */ - private void initSftpClient() { - if (mJSch == null) { - mJSch = new JSch(); - LogUtils.d(TAG, "SFTP客户端(JSch)初始化完成"); - } - // 重置会话和通道,避免连接残留 - mSession = null; - mSftpChannel = null; - } - - /** - * 【推荐】SFTP登录(基于FTPAuthModel实体类,完全兼容原有参数) - * @param ftpAuthModel 登录配置实体类(不能为空,端口默认22,编码默认UTF-8) - * @return 登录成功返回true,失败false - */ - public boolean login(SFTPAuthModel ftpAuthModel) { - // 1. 实体类非空校验 - if (ftpAuthModel == null) { - LogUtils.e(TAG, "SFTP登录失败:FTPAuthModel实体类为null"); - return false; - } - // 2. 核心参数校验(服务器地址不能为空) - if (isParamEmpty(ftpAuthModel.getFtpServer())) { - LogUtils.e(TAG, "SFTP登录失败:服务器地址(ftpServer)不能为空"); - return false; - } - // 3. 若已连接,先断开 - if (isConnected()) { - logout(); - } - // 4. 重新初始化客户端 - initSftpClient(); - - try { - // 获取服务器地址、端口(默认22)、账号、密码 - String host = ftpAuthModel.getFtpServer(); - int port = ftpAuthModel.getFtpPort() <= 0 ? DEFAULT_SFTP_PORT : ftpAuthModel.getFtpPort(); - String username = ftpAuthModel.getFtpUsername(); - String password = ftpAuthModel.getFtpPassword(); - - // SFTP不支持匿名登录,账号密码不能为空(原生接口无匿名登录能力) - if (isParamEmpty(username) || isParamEmpty(password)) { - LogUtils.e(TAG, "SFTP登录失败:SFTP不支持匿名登录,请配置有效账号密码"); - return false; - } - - // 1. 创建JSch会话(原生接口) - mSession = mJSch.getSession(username, host, port); - mSession.setPassword(password); - - // 2. 设置会话属性(跳过SSH密钥校验,适配大部分服务器) - Properties sessionProps = new Properties(); - sessionProps.put("StrictHostKeyChecking", "no"); - sessionProps.put("PreferredAuthentications", "password"); - mSession.setConfig(sessionProps); - - // 3. 设置会话连接超时(原生接口,底层Socket超时) - mSession.setTimeout(CONNECT_TIMEOUT); - - // 4. 建立会话连接(原生接口) - mSession.connect(); - LogUtils.d(TAG, "SFTP会话连接成功:" + host + ":" + port); - - // 5. 打开SFTP通道(类型:sftp,原生接口强转) - mSftpChannel = (ChannelSftp) mSession.openChannel("sftp"); - mSftpChannel.connect(); - - // 6. 设置文件名编码(解决中文乱码,ChannelSftp原生接口) - String charset = isParamEmpty(ftpAuthModel.getFtpCharset()) ? "UTF-8" : ftpAuthModel.getFtpCharset(); - mSftpChannel.setFilenameEncoding(charset); - LogUtils.d(TAG, "SFTP文件名编码设置成功:" + charset); - - LogUtils.i(TAG, "SFTP登录成功,服务器:" + host + ":" + port + ",用户名:" + username); - return true; - - } catch (JSchException e) { - LogUtils.e(TAG, "SFTP登录JSch异常:" + e.getMessage(), e); - logout(); - return false; - } catch (SftpException e) { - // 匹配SftpException原生属性和方法 - LogUtils.e(TAG, "SFTP通道初始化异常:id=" + e.id + ",msg=" + e.getMessage() + ",detail=" + e.toString()); - logout(); - return false; - } - } - - /** - * 【已废弃】原FTP多参数登录方法,适配JSch后保留,推荐使用login(FTPAuthModel) - * @deprecated 请使用基于FTPAuthModel的登录方法 - */ - @Deprecated - public boolean login(String host, int port, String username, String password) { - SFTPAuthModel ftpAuthModel = new SFTPAuthModel(); - ftpAuthModel.setFtpServer(host); - ftpAuthModel.setFtpPort(port <= 0 ? DEFAULT_SFTP_PORT : port); - ftpAuthModel.setFtpUsername(username); - ftpAuthModel.setFtpPassword(password); - return login(ftpAuthModel); - } - - /** - * SFTP登出并断开连接,释放所有资源(严格调用原生disconnect接口) - * @return 登出成功返回true,失败false - */ - public boolean logout() { - boolean isSuccess = true; - // 关闭SFTP通道(原生接口disconnect,非空判断即可) - if (mSftpChannel != null) { - try { - mSftpChannel.disconnect(); - LogUtils.d(TAG, "SFTP通道已断开"); - } catch (Exception e) { - LogUtils.e(TAG, "关闭SFTP通道异常:" + e.getMessage(), e); - isSuccess = false; - } - } - // 关闭JSch会话(原生接口disconnect,非空判断即可) - if (mSession != null) { - try { - mSession.disconnect(); - LogUtils.d(TAG, "SFTP会话已断开"); - } catch (Exception e) { - LogUtils.e(TAG, "关闭SFTP会话异常:" + e.getMessage(), e); - isSuccess = false; - } - } - // 重置客户端,避免资源残留 - initSftpClient(); - if (isSuccess) { - LogUtils.i(TAG, "SFTP登出成功"); - } else { - LogUtils.w(TAG, "SFTP登出失败:部分资源未正常释放"); - } - return isSuccess; - } - - /** - * 强制断开连接(兜底资源释放),同logout方法 - */ - public void disconnect() { - logout(); - } - - /** - * 判断SFTP是否已连接(会话+通道均调用原生isConnected接口) - * @return 已连接返回true,否则false - */ - public boolean isConnected() { - return mSession != null && mSession.isConnected() - && mSftpChannel != null && mSftpChannel.isConnected(); - } - - /** - * 上传文件到SFTP指定路径(覆盖式上传,调用ChannelSftp原生put接口,OVERWRITE模式) - * @param localFilePath 本地文件绝对路径(如/sdcard/test.apk) - * @param remoteFilePath SFTP服务器目标路径(如/ftp/apk/test.apk,需包含文件名) - * @return 上传成功返回true,失败false - */ - public boolean uploadFile(String localFilePath, String remoteFilePath) { - // 前置校验 - if (!isConnected()) { - LogUtils.e(TAG, "文件上传失败:SFTP未连接服务器"); - return false; - } - if (isParamEmpty(localFilePath) || isParamEmpty(remoteFilePath)) { - LogUtils.e(TAG, "文件上传失败:本地/远程路径不能为空"); - return false; - } - File localFile = new File(localFilePath); - if (!localFile.exists() || !localFile.isFile()) { - LogUtils.e(TAG, "文件上传失败:本地文件不存在/非文件,路径:" + localFilePath); - return false; - } - - InputStream fis = null; - try { - // 自动创建远程多级目录(基于原生mkdir/stat接口) - createRemoteDir(remoteFilePath); - // 读取本地文件,上传到SFTP(原生put接口,OVERWRITE覆盖模式) - fis = new FileInputStream(localFile); - mSftpChannel.put(fis, remoteFilePath, ChannelSftp.OVERWRITE); - LogUtils.i(TAG, "文件上传成功:本地" + localFilePath + " → 远程" + remoteFilePath); - return true; - } catch (IOException e) { - LogUtils.e(TAG, "文件上传IO异常:" + e.getMessage(), e); - return false; - } catch (SftpException e) { - // 严格匹配SftpException原生属性:id、getMessage()、toString() - LogUtils.e(TAG, "文件上传SFTP异常:id=" + e.id + ",msg=" + e.getMessage() + ",detail=" + e.toString()); - return false; - } finally { - // 关闭流资源,避免内存泄漏 - closeStream(fis, null); - } - } - - /** - * 从SFTP下载文件到本地指定路径(覆盖式下载,调用ChannelSftp原生get接口) - * @param remoteFilePath SFTP服务器文件路径(如/ftp/apk/test.apk) - * @param localFilePath 本地目标路径(如/sdcard/test.apk,需包含文件名) - * @return 下载成功返回true,失败false - */ - public boolean downloadFile(String remoteFilePath, String localFilePath) { - // 前置校验 - if (!isConnected()) { - LogUtils.e(TAG, "文件下载失败:SFTP未连接服务器"); - return false; - } - if (isParamEmpty(remoteFilePath) || isParamEmpty(localFilePath)) { - LogUtils.e(TAG, "文件下载失败:远程/本地路径不能为空"); - return false; - } - // 校验远程文件是否存在(基于ChannelSftp原生stat接口) - if (!isFileExists(remoteFilePath)) { - LogUtils.e(TAG, "文件下载失败:远程文件不存在,路径:" + remoteFilePath); - return false; - } - - OutputStream fos = null; - try { - // 创建本地多级目录 - File localFile = new File(localFilePath); - File parentDir = localFile.getParentFile(); - if (!parentDir.exists() && !parentDir.mkdirs()) { - LogUtils.e(TAG, "文件下载失败:创建本地目录失败,路径:" + parentDir.getAbsolutePath()); - return false; - } - // 从SFTP读取文件,写入本地(原生get接口) - fos = new FileOutputStream(localFile); - mSftpChannel.get(remoteFilePath, fos); - LogUtils.i(TAG, "文件下载成功:远程" + remoteFilePath + " → 本地" + localFilePath); - return true; - } catch (IOException e) { - LogUtils.e(TAG, "文件下载IO异常:" + e.getMessage(), e); - // 删除未下载完成的本地文件 - new File(localFilePath).delete(); - return false; - } catch (SftpException e) { - // 严格匹配SftpException原生属性:id、getMessage()、toString() - LogUtils.e(TAG, "文件下载SFTP异常:id=" + e.id + ",msg=" + e.getMessage() + ",detail=" + e.toString()); - // 删除未下载完成的本地文件 - new File(localFilePath).delete(); - return false; - } finally { - // 关闭流资源,避免内存泄漏 - closeStream(null, fos); - } - } - - /** - * 列举SFTP指定文件夹下的所有文件/文件夹(返回ChannelSftp原生Vector,过滤.和..) - * @param remoteDir SFTP服务器目录路径(如/ftp/apk/,结尾带/或不带均可) - * @return 成功返回原生Vector,失败返回空Vector - */ - @SuppressWarnings("rawtypes") - public Vector listDir(String remoteDir) { - Vector fileList = new Vector(); - // 前置校验 - if (!isConnected()) { - LogUtils.e(TAG, "列举目录失败:SFTP未连接服务器"); - return fileList; - } - if (isParamEmpty(remoteDir)) { - LogUtils.e(TAG, "列举目录失败:远程目录路径不能为空"); - return fileList; - } - // 校验目录是否存在(基于ChannelSftp原生stat接口) - if (!isDirExists(remoteDir)) { - LogUtils.e(TAG, "列举目录失败:远程目录不存在,路径:" + remoteDir); - return fileList; - } - - try { - // 列举目录下所有文件/文件夹(调用ChannelSftp原生ls接口,返回原生Vector) - Vector vector = mSftpChannel.ls(remoteDir); - if (vector != null && vector.size() > 0) { - for (Object obj : vector) { - // 过滤.和..上级目录,仅保留有效文件/目录 - ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) obj; - String fileName = entry.getFilename(); - if (!".".equals(fileName) && !"..".equals(fileName)) { - fileList.add(obj); - } - } - } - LogUtils.i(TAG, "列举目录成功:" + remoteDir + ",共" + fileList.size() + "个文件/文件夹"); - } catch (SftpException e) { - // 严格匹配SftpException原生属性:id、getMessage()、toString() - LogUtils.e(TAG, "列举目录SFTP异常:id=" + e.id + ",msg=" + e.getMessage() + ",detail=" + e.toString()); - } - return fileList; - } - - /** - * 判断SFTP服务器上**文件**是否存在(基于ChannelSftp原生stat接口,匹配SftpException原生异常) - * @param remoteFilePath SFTP服务器文件路径(如/ftp/apk/test.apk) - * @return 存在且为文件返回true,否则false - */ - public boolean isFileExists(String remoteFilePath) { - // 前置校验 - if (!isConnected()) { - LogUtils.e(TAG, "判断文件存在性失败:SFTP未连接服务器"); - return false; - } - if (isParamEmpty(remoteFilePath)) { - LogUtils.e(TAG, "判断文件存在性失败:远程文件路径不能为空"); - return false; - } - - try { - // 调用ChannelSftp原生stat接口获取属性,不存在会抛出SSH_FX_NO_SUCH_FILE异常 - SftpATTRS attrs = mSftpChannel.stat(remoteFilePath); - // 原生isReg()判断是否为文件 - return attrs.isReg(); - } catch (SftpException e) { - // 仅匹配原生异常码SSH_FX_NO_SUCH_FILE(2):文件/目录不存在,不记错误日志 - if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) { - LogUtils.e(TAG, "判断文件存在性SFTP异常:id=" + e.id + ",msg=" + e.getMessage() + ",detail=" + e.toString()); - } - return false; - } - } - - /** - * 判断SFTP服务器上**文件夹**是否存在(基于ChannelSftp原生stat接口,匹配SftpException原生异常) - * @param remoteDir SFTP服务器目录路径(如/ftp/apk/,结尾带/或不带均可) - * @return 存在且为目录返回true,否则false - */ - public boolean isDirExists(String remoteDir) { - // 前置校验 - if (!isConnected()) { - LogUtils.e(TAG, "判断目录存在性失败:SFTP未连接服务器"); - return false; - } - if (isParamEmpty(remoteDir)) { - LogUtils.e(TAG, "判断目录存在性失败:远程目录路径不能为空"); - return false; - } - - try { - // 调用ChannelSftp原生stat接口获取属性,不存在会抛出SSH_FX_NO_SUCH_FILE异常 - SftpATTRS attrs = mSftpChannel.stat(remoteDir); - // 原生isDir()判断是否为目录 - return attrs.isDir(); - } catch (SftpException e) { - // 仅匹配原生异常码SSH_FX_NO_SUCH_FILE(2):文件/目录不存在,不记错误日志 - if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) { - LogUtils.e(TAG, "判断目录存在性SFTP异常:id=" + e.id + ",msg=" + e.getMessage() + ",detail=" + e.toString()); - } - return false; - } - } - - // ===================================== 内部工具方法(仅调用原生接口) ===================================== - /** - * 递归创建SFTP远程多级目录(基于ChannelSftp原生mkdir/stat接口,不存在则创建) - * @param remoteFilePath SFTP远程文件路径/目录路径 - */ - private void createRemoteDir(String remoteFilePath) { - if (!isConnected()) { - LogUtils.e(TAG, "创建远程目录失败:SFTP未连接服务器"); - return; - } - try { - // 提取目录路径(文件路径→目录路径,目录路径直接使用) - String remoteDir = remoteFilePath.lastIndexOf("/") > 0 - ? remoteFilePath.substring(0, remoteFilePath.lastIndexOf("/")) - : remoteFilePath; - // 按/分割多级目录,递归创建(避免多级目录不存在) - String[] dirs = remoteDir.split("/"); - StringBuilder currentDir = new StringBuilder(); - for (String dir : dirs) { - if (isParamEmpty(dir)) { - continue; - } - currentDir.append("/").append(dir); - String dirPath = currentDir.toString(); - // 目录不存在则调用ChannelSftp原生mkdir创建 - if (!isDirExists(dirPath)) { - mSftpChannel.mkdir(dirPath); - LogUtils.d(TAG, "创建SFTP远程目录成功:" + dirPath); - } - } - } catch (SftpException e) { - // 严格匹配SftpException原生属性:id、getMessage()、toString() - LogUtils.e(TAG, "创建远程目录SFTP异常:id=" + e.id + ",msg=" + e.getMessage() + ",detail=" + e.toString()); - } - } - - /** - * 关闭流资源(通用工具方法,Java7原生IO,避免内存泄漏) - * @param is 输入流(可为null) - * @param os 输出流(可为null) - */ - private void closeStream(InputStream is, OutputStream os) { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - LogUtils.e(TAG, "关闭输入流异常:" + e.getMessage(), e); - } - } - if (os != null) { - try { - os.close(); - } catch (IOException e) { - LogUtils.e(TAG, "关闭输出流异常:" + e.getMessage(), e); - } - } - } - - /** - * 判断参数是否为空(null/空字符串/全空格,Java7原生字符串操作) - * @param param 待判断参数 - * @return 为空返回true,否则false - */ - private boolean isParamEmpty(String param) { - return param == null || param.trim().isEmpty(); - } -} - 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 8d94b53..050a8b6 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,13 +12,11 @@ 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.APPValidationDialog; import cc.winboll.studio.libappbase.models.APPInfo; /** @@ -328,14 +326,7 @@ public class AboutView extends LinearLayout { ToastUtils.show("已取消调试状态,重启应用可生效。"); } }); - // 正版校验弹窗 - ibSigngetDialog.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "ibSigngetDialog onClick:唤起应用正版校验弹窗"); - new APPValidationDialog(mContext, mszAppName, mszAppVersionName).show(); - } - }); + // 调试地址配置弹窗 ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() { @Override