From 1cadc4ed93f1362a2e138cb3ceb48e21c8974b00 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Mon, 1 Dec 2025 15:18:26 +0800 Subject: [PATCH] 20251201_151821_718 --- powerbell/build.properties | 4 +- .../BackgroundSettingsActivity.java | 188 +++++++----- .../powerbell/model/BackgroundBean.java | 92 ++++-- .../utils/BackgroundSourceUtils.java | 274 +++++++----------- .../powerbell/views/BackgroundView.java | 4 +- ...e.xml => activity_background_settings.xml} | 0 6 files changed, 291 insertions(+), 271 deletions(-) rename powerbell/src/main/res/layout/{activity_backgroundpicture.xml => activity_background_settings.xml} (100%) diff --git a/powerbell/build.properties b/powerbell/build.properties index c2245232..9a1deffb 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Dec 01 02:15:51 GMT 2025 +#Mon Dec 01 07:14:25 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=48 +buildCount=50 baseBetaVersion=15.11.13 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java index 7ecd11f4..47a68f28 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -86,7 +86,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_backgroundpicture); + setContentView(R.layout.activity_background_settings); // 初始化核心控件 bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1); // 初始化工具类(文件管理全依赖此类) @@ -104,13 +104,25 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 初始化按钮点击事件(仅UI交互,无文件逻辑) initClickListeners(); // 初始化预览 - initPreview(); + // 关键:窗口启动时,将正式Bean的内容完整拷贝到预览Bean(覆盖旧预览Bean) + initPreviewBeanFromFormal(); // 处理分享图片意图 handleShareIntent(); LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(精简版,文件管理依赖BackgroundSourceUtils)"); } + /** + * 初始化预览Bean:从正式Bean拷贝所有属性(确保每次操作都是正式图的副本) + */ + private void initPreviewBeanFromFormal() { + LogUtils.d(TAG, "【Bean初始化】正式Bean → 预览Bean(拷贝初始化)"); + mBgSourceUtils.setCurrentSourceToPreview(); + // 加载预览Bean中的图片(窗口启动时显示正式图的预览) + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【Bean初始化】预览Bean初始化完成,当前预览路径:" + mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath()); + } + /** * 初始化工具栏(仅UI逻辑) */ @@ -145,15 +157,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg /** * 初始化预览(调用工具类同步正式背景到预览) */ - private void initPreview() { - mBgSourceUtils.setCurrentSourceToPreview(); - if (bvPreviewBackground != null) { - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【初始化】预览视图已加载,BackgroundView 状态:正常"); - } else { - LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败"); - } - } +// private void initPreview() { +// mBgSourceUtils.setCurrentSourceToPreview(); +// if (bvPreviewBackground != null) { +// bvPreviewBackground.reloadPreviewBackground(); +// LogUtils.d(TAG, "【初始化】预览视图已加载,BackgroundView 状态:正常"); +// } else { +// LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败"); +// } +// } /** * 处理分享图片意图(仅UI逻辑,文件处理调用工具类) @@ -461,6 +473,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 确保父目录存在(调用工具类创建目录,避免重复代码) File parentDir = fRecivedPicture.getParentFile(); if (!parentDir.exists()) { + parentDir.mkdirs(); mBgSourceUtils.copyFile(new File(""), parentDir); // 复用工具类目录创建逻辑 LogUtils.d(TAG, "【压缩准备】目标目录已通过工具类创建:" + parentDir.getAbsolutePath()); } @@ -528,7 +541,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg public void startCropImageActivity(boolean isCropFree) { LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - previewBean.setIsUseScaledCompress(true); + previewBean.setIsUseBackgroundScaledCompressFile(true); mBgSourceUtils.saveSettings(); // 1. 预览图片有效性校验(保留强化校验逻辑) @@ -985,11 +998,24 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - // 同步到预览并启动裁剪(此时文件已存在,避免传入目录路径) + // 关键修复:选图后生成压缩图(确保预览时压缩图存在) + LogUtils.d(TAG, "【选图压缩】开始生成预览压缩图"); + Bitmap selectBitmap = BitmapFactory.decodeFile(selectedFile.getAbsolutePath()); + if (selectBitmap != null && !selectBitmap.isRecycled()) { + compressQualityToRecivedPicture(selectBitmap); // 生成压缩图 + selectBitmap.recycle(); // 回收Bitmap,避免OOM + } else { + ToastUtils.show("选图后压缩图生成失败"); + LogUtils.e(TAG, "【选图压缩失败】无法解析选图文件为Bitmap"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 同步到预览并启动裁剪(此时正式图和压缩图均已存在) mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString()); bvPreviewBackground.reloadPreviewBackground(); startCropImageActivity(false); - LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪"); + LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪(正式图+压缩图均已生成)"); } /** @@ -1062,14 +1088,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * 保存裁剪后的Bitmap(调用工具类保存,精简文件逻辑) */ private void saveCropBitmap(Bitmap bitmap) { - LogUtils.d(TAG, "【保存启动】开始保存裁剪图片"); + LogUtils.d(TAG, "【保存启动】开始保存裁剪图片(仅更新预览Bean,不影响正式Bean)"); if (bitmap == null || bitmap.isRecycled()) { ToastUtils.show("裁剪图片为空"); mBgSourceUtils.clearCropTempFiles(); return; } - // 内存优化:大图片缩放(调整阈值为5MB,更适配低内存机型) + // 内存优化:大图片缩放(保留原有逻辑) Bitmap scaledBitmap = bitmap; int originalSize = bitmap.getByteCount() / 1024 / 1024; // 转换为MB if (originalSize > 5) { // 超过5MB自动缩放 @@ -1085,77 +1111,98 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } FileOutputStream fos = null; - // 新增:定义裁剪后新文件的文件名(与工具类保持一致的唯一命名规则) - String cropResultFileName = FileUtils.createUniqueFileName(new File("crop_result.jpg")); try { - // 从工具类获取预览图片目录(统一路径管理) - String backgroundSourceDir = mBgSourceUtils.getBackgroundSourceDirPath(); - // 裁剪后新图的目标路径(正式图+压缩图,路径统一) - String cropResultFilePath = backgroundSourceDir + File.separator + cropResultFileName; - String cropCompressFilePath = backgroundSourceDir + File.separator + "ScaledCompress_" + cropResultFileName; + // 1. 生成唯一文件名(仅用于预览图,正式图由退出时拷贝生成) + String uniqueFileName = FileUtils.createUniqueFileName(new File("background.jpg")); + String compressFileName = "ScaledCompress_" + uniqueFileName; // 预览压缩图文件名 - File targetCompressFile = new File(cropCompressFilePath); + // 2. 定义预览图路径(统一目录,仅操作预览相关文件) + String backgroundDir = mBgSourceUtils.getBackgroundSourceDirPath(); + File targetPreviewOriginalFile = new File(backgroundDir, uniqueFileName); // 裁剪后预览正式图 + File targetPreviewCompressFile = new File(backgroundDir, compressFileName); // 裁剪后预览压缩图 - // 确保目录存在(调用工具类) - File parentDir = targetCompressFile.getParentFile(); + // 确保目录存在(调用工具类,避免创建失败) + File parentDir = targetPreviewOriginalFile.getParentFile(); if (!parentDir.exists()) { mBgSourceUtils.copyFile(new File(""), parentDir); } - // 清理旧文件(调用工具类) - if (targetCompressFile.exists()) { - mBgSourceUtils.clearOldFileByExternal(targetCompressFile, "旧裁剪结果文件"); + // 3. 保存裁剪后的预览正式图(仅写入预览目录,不触碰正式图) + if (targetPreviewOriginalFile.exists()) { + mBgSourceUtils.clearOldFileByExternal(targetPreviewOriginalFile, "旧预览正式图"); } - targetCompressFile.createNewFile(); - mBgSourceUtils.setFilePermissions(targetCompressFile); // 调用工具类设置权限 + targetPreviewOriginalFile.createNewFile(); + mBgSourceUtils.setFilePermissions(targetPreviewOriginalFile); - // 压缩保存 - fos = new FileOutputStream(targetCompressFile); - Bitmap.CompressFormat format = targetCompressFile.getName().endsWith(".png") ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG; - boolean success = scaledBitmap.compress(format, 80, fos); + fos = new FileOutputStream(targetPreviewOriginalFile); + Bitmap.CompressFormat format = targetPreviewOriginalFile.getName().endsWith(".png") ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG; + boolean previewOriginalSaveSuccess = scaledBitmap.compress(format, 80, fos); fos.flush(); - fos.getFD().sync(); // 强制同步到磁盘,确保文件100%保存完成 + fos.getFD().sync(); // 强制同步磁盘,确保文件写入完成 + fos.close(); // 关闭预览正式图流 - if (success) { - ToastUtils.show("图片保存成功"); - // 核心修复1:同步预览Bean的「文件名+压缩图路径」(两者必须匹配) + // 4. 生成预览压缩图(预览Bean依赖此图,必须生成) + LogUtils.d(TAG, "【裁剪压缩】开始生成预览压缩图:" + targetPreviewCompressFile.getAbsolutePath()); + FileOutputStream compressFos = new FileOutputStream(targetPreviewCompressFile); + boolean previewCompressSaveSuccess = scaledBitmap.compress(format, 50, compressFos); // 压缩质量50%(按需调整) + compressFos.flush(); + compressFos.getFD().sync(); + compressFos.close(); + LogUtils.d(TAG, "【裁剪压缩】预览压缩图保存" + (previewCompressSaveSuccess ? "成功" : "失败") + ",大小:" + targetPreviewCompressFile.length() + "bytes"); + + // 5. 关键逻辑:仅更新预览Bean(正式Bean不改动,留待退出时确认) + if (previewOriginalSaveSuccess && previewCompressSaveSuccess + && targetPreviewOriginalFile.length() > 100 + && targetPreviewCompressFile.length() > 100) { + // 获取当前预览Bean(窗口启动时已由正式Bean拷贝而来) BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - previewBean.setBackgroundFileName(cropResultFileName); // 同步正式文件名(关键!) - previewBean.setBackgroundScaledCompressFileName("ScaledCompress_" + cropResultFileName); // 同步压缩文件名 - previewBean.setBackgroundScaledCompressFilePath(cropCompressFilePath); // 同步压缩路径 - previewBean.setIsUseBackgroundFile(true); - mBgSourceUtils.saveSettings(); // 持久化配置,确保BackgroundView读取时生效 + // 仅更新预览Bean的路径信息(不操作正式Bean) + previewBean.setBackgroundFileName(uniqueFileName); // 预览正式图文件名 + previewBean.setBackgroundFilePath(targetPreviewOriginalFile.getAbsolutePath()); // 预览正式图路径 + previewBean.setBackgroundScaledCompressFileName(compressFileName); // 预览压缩图文件名 + previewBean.setBackgroundScaledCompressFilePath(targetPreviewCompressFile.getAbsolutePath()); // 预览压缩图路径 + previewBean.setIsUseBackgroundFile(true); // 标记预览图可用 + previewBean.setIsUseBackgroundScaledCompressFile(false); // 标记:未确认成为正式图(关键标记) + mBgSourceUtils.saveSettings(); // 仅持久化预览Bean(不保存正式Bean) - // 核心修复2:调用工具类同步新图到预览(确保工具类路径与Bean一致) - mBgSourceUtils.saveFileToPreviewBean(targetCompressFile, cropCompressFilePath); + // 同步预览文件到预览Bean目录(工具类仅操作预览相关文件) + mBgSourceUtils.saveFileToPreviewBean(targetPreviewOriginalFile, targetPreviewOriginalFile.getAbsolutePath()); + mBgSourceUtils.saveFileToPreviewBean(targetPreviewCompressFile, targetPreviewCompressFile.getAbsolutePath()); - // 核心修复3:调整刷新时机——文件100%保存完成后,再触发双重刷新(避免过早刷新) + ToastUtils.show("预览图片保存成功(未设为正式背景)"); + // 触发预览刷新(仅加载预览Bean中的路径) doubleRefreshPreview(); } else { - ToastUtils.show("图片保存失败"); - LogUtils.e(TAG, "【保存失败】Bitmap压缩失败"); - // 回滚配置 + ToastUtils.show("预览图片保存失败"); + LogUtils.e(TAG, "【预览保存失败】正式图:" + previewOriginalSaveSuccess + ",压缩图:" + previewCompressSaveSuccess); + // 回滚预览Bean:标记为未使用文件 BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); previewBean.setIsUseBackgroundFile(false); + previewBean.setIsUseBackgroundScaledCompressFile(false); mBgSourceUtils.saveSettings(); } } catch (Exception e) { - LogUtils.e(TAG, "【保存异常】" + e.getMessage(), e); - ToastUtils.show("图片保存失败"); + LogUtils.e(TAG, "【裁剪保存异常】" + e.getMessage(), e); + ToastUtils.show("预览图片保存失败"); + // 异常时回滚预览Bean + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setIsUseBackgroundFile(false); + previewBean.setIsUseBackgroundScaledCompressFile(false); + mBgSourceUtils.saveSettings(); } finally { - // 关闭流 + // 关闭流资源 if (fos != null) { try { fos.close(); } catch (IOException e) { - LogUtils.e(TAG, "【保存异常】流关闭失败:" + e.getMessage()); + LogUtils.e(TAG, "【裁剪保存异常】流关闭失败:" + e.getMessage()); } } // 回收Bitmap if (scaledBitmap != null && !scaledBitmap.isRecycled()) { scaledBitmap.recycle(); } - // 清理裁剪临时文件(调用工具类) + // 清理裁剪临时文件(仅清理缓存,不影响预览/正式文件) mBgSourceUtils.clearCropTempFiles(); } } @@ -1179,32 +1226,39 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg runOnUiThread(new Runnable() { @Override public void run() { - // 新增:校验预览图是否存在(避免文件未保存完成就刷新) - final String previewFilePath = mBgSourceUtils.getPreviewBackgroundFilePath(); - File previewFile = new File(previewFilePath); - if (!previewFile.exists() || previewFile.length() <= 100) { - LogUtils.w(TAG, "【预览刷新】预览图未就绪,延迟500ms后刷新"); - // 延迟500ms重试(适配大图片保存延迟) + // 1. 同时校验正式图和压缩图(两者都存在才刷新) + String previewOriginalPath = mBgSourceUtils.getPreviewBackgroundFilePath(); + final String previewCompressPath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); + File originalFile = new File(previewOriginalPath); + File compressFile = new File(previewCompressPath); + + // 校验逻辑:正式图存在 + 压缩图存在 + 大小均>100字节(避免空文件) + boolean isPreviewReady = originalFile.exists() && originalFile.length() > 100 + && compressFile.exists() && compressFile.length() > 100; + + if (!isPreviewReady) { + LogUtils.w(TAG, "【预览刷新】预览图未就绪(正式图:" + originalFile.exists() + ",压缩图:" + compressFile.exists() + "),延迟500ms重试"); + // 延迟500ms重试(适配压缩图生成延迟) new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】延迟重试刷新(500ms)"); + LogUtils.d(TAG, "【预览刷新】延迟重试刷新(500ms),压缩图路径:" + previewCompressPath); } }, 500); return; } - // 第一次刷新(立即) + // 2. 第一次刷新(立即) bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第一次刷新(立即),路径:" + previewFilePath); + LogUtils.d(TAG, "【预览刷新】第一次刷新(立即),正式图路径:" + previewOriginalPath + ",压缩图路径:" + previewCompressPath); - // 第二次刷新(延迟300ms,适配MIUI渲染延迟) + // 3. 第二次刷新(延迟300ms,适配MIUI渲染延迟) new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第二次刷新(延迟300ms),路径:" + previewFilePath); + LogUtils.d(TAG, "【预览刷新】第二次刷新(延迟300ms),压缩图路径:" + previewCompressPath); } }, 300); } @@ -1347,7 +1401,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【图片接收】已启动自由裁剪,裁剪路径由工具类管理"); } }; - + /** * 重写finish方法,确保所有退出场景都触发确认提示(仅操作Bean,无文件逻辑) */ diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java index 3657925e..33f6ef2e 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java @@ -16,16 +16,18 @@ public class BackgroundBean extends BaseBean { // 核心字段:背景图片文件名(对应应用私有目录下的图片文件,与BackgroundSettingsActivity的_mSourceCroppedFile匹配) private String backgroundFileName = ""; + // 核心字段:背景图片完整路径(解决仅存文件名导致的路径拼接错误,与backgroundScaledCompressFilePath对应) + private String backgroundFilePath = ""; // 附加字段:图片信息(如Uri、网络地址等,仅作备注,不参与路径生成) private String backgroundFileInfo = ""; // 控制字段:是否启用背景图片(true-显示背景图,false-显示透明背景) private boolean isUseBackgroundFile = false; // 核心字段:压缩后背景图片文件名(对应应用私有目录下的压缩图片,与saveCropBitmap的压缩图匹配) private String backgroundScaledCompressFileName = ""; - // 新增:压缩后背景图片完整路径(解决仅存文件名导致的路径拼接错误,适配BackgroundSettingsActivity的私有目录) + // 核心字段:压缩后背景图片完整路径(解决仅存文件名导致的路径拼接错误,适配BackgroundSettingsActivity的私有目录) private String backgroundScaledCompressFilePath = ""; - // 控制字段:是否启用压缩背景图(true-加载压缩图,false-加载原图) - private boolean isUseScaledCompress = false; + // 重命名字段:是否启用压缩背景图(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile,语义更清晰) + private boolean isUseBackgroundScaledCompressFile = false; // 裁剪比例字段:背景图宽高比(默认1:1,用于固定比例裁剪) private int backgroundWidth = 100; private int backgroundHeight = 100; @@ -38,7 +40,7 @@ public class BackgroundBean extends BaseBean { public BackgroundBean() { } - // ====================================== Getter/Setter 方法(全字段,确保序列化/反序列化完整)====================================== + // ====================================== Getter/Setter 方法(全字段,含重命名+新增字段)====================================== public String getBackgroundFileName() { return backgroundFileName; } @@ -47,6 +49,14 @@ public class BackgroundBean extends BaseBean { this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName; // 防null,避免空指针 } + public String getBackgroundFilePath() { + return backgroundFilePath; + } + + public void setBackgroundFilePath(String backgroundFilePath) { + this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath; // 防null,避免路径拼接错误 + } + public String getBackgroundFileInfo() { return backgroundFileInfo; } @@ -71,9 +81,6 @@ public class BackgroundBean extends BaseBean { this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName; // 防null } - /** - * 新增:压缩图完整路径 Getter/Setter(适配BackgroundSettingsActivity的saveCropBitmap方法) - */ public String getBackgroundScaledCompressFilePath() { return backgroundScaledCompressFilePath; } @@ -82,12 +89,16 @@ public class BackgroundBean extends BaseBean { this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath; // 防null,避免路径错误 } - public boolean isUseScaledCompress() { - return isUseScaledCompress; + /** + * 重命名:原isUseScaledCompress → 新isUseBackgroundScaledCompressFile(Getter/Setter同步修改) + * 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆 + */ + public boolean isUseBackgroundScaledCompressFile() { + return isUseBackgroundScaledCompressFile; } - public void setIsUseScaledCompress(boolean isUseScaledCompress) { - this.isUseScaledCompress = isUseScaledCompress; + public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) { + this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile; } public int getBackgroundWidth() { @@ -114,45 +125,54 @@ public class BackgroundBean extends BaseBean { this.pixelColor = pixelColor; } - // ====================================== 序列化/反序列化方法(适配JSON读写,确保数据持久化完整)====================================== + // ====================================== 序列化/反序列化方法(适配重命名字段,兼容旧版本)====================================== @Override public String getName() { return BackgroundBean.class.getName(); // 必须重写,BaseBean序列化时需类名标识 } /** - * 将Bean数据写入JSON(序列化,保存到文件) - * 确保所有字段都被写入,无遗漏(新增backgroundScaledCompressFilePath字段) + * 序列化:同步重命名字段(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile) + * 确保新字段能正常持久化,同时兼容旧版本JSON(可选:保留旧字段写入,避免旧版本读取异常) */ @Override public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { super.writeThisToJsonWriter(jsonWriter); BackgroundBean bean = this; jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName()); + jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath()); // 新增字段:背景原图完整路径 jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo()); jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile()); jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName()); - jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath()); // 新增字段序列化 - jsonWriter.name("isUseScaledCompress").value(bean.isUseScaledCompress()); + jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath()); + // 关键:新字段序列化(核心) + jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile()); + // 兼容旧版本:保留旧字段名写入(可选,避免旧版本Bean读取时缺失字段) + jsonWriter.name("isUseScaledCompress").value(bean.isUseBackgroundScaledCompressFile()); jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth()); jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight()); jsonWriter.name("pixelColor").value(bean.getPixelColor()); } /** - * 从JSON读取数据到Bean(反序列化,从文件加载) - * 确保所有字段都被读取,兼容旧版本(无字段时设默认值,新增字段兼容) + * 反序列化:同步处理重命名字段(兼容旧版本JSON,新旧字段都能读取) + * 逻辑:优先读取新字段,若新字段不存在则读取旧字段(确保升级后旧配置仍有效) */ @Override public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { BackgroundBean bean = new BackgroundBean(); jsonReader.beginObject(); + // 临时变量:存储旧字段值(用于兼容) + boolean tempUseScaledCompress = false; while (jsonReader.hasNext()) { String name = jsonReader.nextName(); switch (name) { case "backgroundFileName": bean.setBackgroundFileName(jsonReader.nextString()); break; + case "backgroundFilePath": + bean.setBackgroundFilePath(jsonReader.nextString()); // 新增字段:读取背景原图完整路径 + break; case "backgroundFileInfo": bean.setBackgroundFileInfo(jsonReader.nextString()); break; @@ -163,10 +183,15 @@ public class BackgroundBean extends BaseBean { bean.setBackgroundScaledCompressFileName(jsonReader.nextString()); break; case "backgroundScaledCompressFilePath": - bean.setBackgroundScaledCompressFilePath(jsonReader.nextString()); // 新增字段反序列化 + bean.setBackgroundScaledCompressFilePath(jsonReader.nextString()); break; + // 关键:读取新字段(优先) + case "isUseBackgroundScaledCompressFile": + bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean()); + break; + // 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值) case "isUseScaledCompress": - bean.setIsUseScaledCompress(jsonReader.nextBoolean()); + tempUseScaledCompress = jsonReader.nextBoolean(); break; case "backgroundWidth": bean.setBackgroundWidth(jsonReader.nextInt()); @@ -178,39 +203,52 @@ public class BackgroundBean extends BaseBean { bean.setPixelColor(jsonReader.nextInt()); break; default: - jsonReader.skipValue(); // 跳过未知字段,兼容旧版本Bean + jsonReader.skipValue(); // 跳过未知字段,兼容旧版本Bean(避免崩溃) break; } } jsonReader.endObject(); + // 兼容逻辑:若新字段未被赋值(旧版本JSON无此字段),则用旧字段值填充 + if (!jsonReader.toString().contains("isUseBackgroundScaledCompressFile")) { + bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress); + } return bean; } - // ====================================== 辅助方法(优化BackgroundSettingsActivity调用体验)====================================== + // ====================================== 辅助方法(同步更新重命名字段)====================================== /** - * 重置背景配置(适配“取消背景”功能,避免残留无效数据) + * 重置背景配置(适配“取消背景”功能,同步重置重命名字段) */ public void resetBackgroundConfig() { this.backgroundFileName = ""; + this.backgroundFilePath = ""; // 新增:重置背景原图完整路径 this.backgroundScaledCompressFileName = ""; this.backgroundScaledCompressFilePath = ""; this.backgroundFileInfo = ""; this.isUseBackgroundFile = false; - this.isUseScaledCompress = false; + this.isUseBackgroundScaledCompressFile = false; // 重命名字段:重置为false this.backgroundWidth = 100; this.backgroundHeight = 100; } /** * 检查背景配置是否有效(适配BackgroundSettingsActivity的预览/保存校验) + * 同步使用重命名字段判断压缩图是否启用 * @return true-配置有效(可显示背景图),false-配置无效 */ public boolean isBackgroundConfigValid() { - // 启用背景图时,需确保文件名或完整路径非空 + // 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空 if (!isUseBackgroundFile) { return false; } - return !((backgroundFileName.isEmpty() && backgroundScaledCompressFileName.isEmpty()) - || backgroundScaledCompressFilePath.isEmpty()); + // 原图校验:路径非空 或 文件名非空 + boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty(); + // 压缩图校验:启用压缩图时,路径/文件名需非空 + boolean isCompressValid = true; + if (isUseBackgroundScaledCompressFile()) { // 重命名字段:判断是否启用压缩图 + isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty(); + } + // 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效 + return isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid; } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java index cdbca54a..7579160a 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -8,16 +8,12 @@ import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; /** * @Author ZhanGSKen * @Date 2024/07/18 12:07:20 - * @Describe 背景图片工具集(调整版:图片存储→/Pictures/PowerBell,JSON→应用外置存储) + * @Describe 背景图片工具集(精简版:复用FileUtils,聚焦业务逻辑) */ public class BackgroundSourceUtils { @@ -26,17 +22,17 @@ public class BackgroundSourceUtils { private static final String CROP_TEMP_DIR_NAME = "cache"; // 裁剪缓存目录(基础目录下) private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; // 裁剪输入临时文件 private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; // 裁剪输出结果文件 - private static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容 - // 图片操作基础目录(核心调整:系统公共图片目录) + public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容 + // 图片操作基础目录(核心:系统公共图片目录) private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell"; // 1. 静态实例加volatile,禁止指令重排,保证可见性(双重校验锁单例核心) private static volatile BackgroundSourceUtils sInstance; private Context mContext; private File currentBackgroundBeanFile; - private BackgroundBean currentBackgroundBean; + private BackgroundBean currentBackgroundBean; // 正式Bean:独立实例 private File previewBackgroundBeanFile; - private BackgroundBean previewBackgroundBean; + private BackgroundBean previewBackgroundBean; // 预览Bean:独立实例(与正式Bean完全分离) // 2. 统一文件目录(分两类:图片目录→系统公共目录,JSON目录→应用外置存储) // 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/) @@ -63,7 +59,7 @@ public class BackgroundSourceUtils { initJsonDirs(); // 初始化JSON配置目录(应用外置存储) // 初始化所有文件(裁剪临时文件/结果文件等) initAllFiles(); - // 加载配置(正式/预览Bean,JSON目录下) + // 加载配置(确保正式/预览Bean是两份独立实例) loadSettings(); } @@ -80,7 +76,7 @@ public class BackgroundSourceUtils { } /** - * 初始化图片操作目录(核心调整:系统公共图片目录 /Pictures/PowerBell/) + * 初始化图片操作目录(核心:系统公共图片目录 /Pictures/PowerBell/) */ private void initPictureDirs() { // 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell @@ -90,7 +86,7 @@ public class BackgroundSourceUtils { // 3. 裁剪缓存目录:基础目录下/cache(所有裁剪操作在此目录) fPictureCacheDir = new File(fPictureBaseDir, CROP_TEMP_DIR_NAME); - // 4. 递归创建目录(系统公共目录需强制授权,确保创建成功) + // 4. 递归创建目录(复用FileUtils间接创建,简化逻辑) createDirWithPermission(fPictureBaseDir, "图片基础目录(/Pictures/PowerBell)"); createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下)"); createDirWithPermission(fPictureCacheDir, "裁剪缓存目录(基础目录/cache)"); @@ -112,11 +108,11 @@ public class BackgroundSourceUtils { fModelDir = new File(fUtilsDir, "ModelDir"); createDirWithPermission(fModelDir, "JSON配置目录(应用外置存储)"); - // 3. 初始化JSON文件对象 + // 3. 初始化JSON文件对象(两份独立文件,对应两份Bean实例) currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json"); previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json"); - LogUtils.d(TAG, "【JSON目录初始化】完成:目录=" + fModelDir.getAbsolutePath()); + LogUtils.d(TAG, "【JSON目录初始化】完成:目录=" + fModelDir.getAbsolutePath() + ",正式JSON=" + currentBackgroundBeanFile.getName() + ",预览JSON=" + previewBackgroundBeanFile.getName()); } /** @@ -128,7 +124,7 @@ public class BackgroundSourceUtils { // 2. 裁剪结果文件(图片存储目录下,最终保存的裁剪图) cropResultFile = new File(fBackgroundSourceDir, CROP_RESULT_FILE_NAME); - // 3. 初始化时清理旧文件(避免文件锁定/权限残留) + // 3. 初始化时清理旧文件(复用FileUtils简化清理逻辑) clearOldFile(cropTempFile, "旧裁剪临时文件(/Pictures/PowerBell/cache)"); clearOldFile(cropResultFile, "旧裁剪结果文件(/Pictures/PowerBell/BackgroundSource)"); @@ -197,24 +193,25 @@ public class BackgroundSourceUtils { } /** - * 加载背景图片配置数据(JSON文件仍在应用外置存储,不改变) + * 加载背景图片配置数据(核心:确保current/preview是两份独立的BackgroundBean实例) */ void loadSettings() { - // 加载正式Bean + // 1. 加载正式Bean(独立实例:从currentBackgroundBean.json加载,不存在则新建) currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); if (currentBackgroundBean == null) { - currentBackgroundBean = new BackgroundBean(); + currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化 BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); - LogUtils.d(TAG, "【配置管理】正式背景Bean不存在,创建默认Bean"); + LogUtils.d(TAG, "【配置管理】正式背景Bean不存在,创建独立实例并保存到JSON"); } - // 加载预览Bean + // 2. 加载预览Bean(独立实例:从previewBackgroundBean.json加载,不存在则新建,与正式Bean完全分离) previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); if (previewBackgroundBean == null) { - previewBackgroundBean = new BackgroundBean(); + previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化 BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); - LogUtils.d(TAG, "【配置管理】预览背景Bean不存在,创建默认Bean"); + LogUtils.d(TAG, "【配置管理】预览背景Bean不存在,创建独立实例并保存到JSON"); } + LogUtils.d(TAG, "【配置管理】两份Bean实例初始化完成:正式Bean=" + currentBackgroundBean.hashCode() + ",预览Bean=" + previewBackgroundBean.hashCode() + "(hash不同证明实例独立)"); } // ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------ @@ -230,9 +227,7 @@ public class BackgroundSourceUtils { * 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) */ public String getCurrentBackgroundFilePath() { - // 移除:loadSettings(); // 关键修复:避免每次调用都重新加载Bean,导致字段被重置为空 String fileName = currentBackgroundBean.getBackgroundFileName(); - // 强化校验:若文件名为空,返回空路径(避免拼接目录路径) if (TextUtils.isEmpty(fileName)) { LogUtils.e(TAG, "【路径管理】正式背景文件名为空,返回空路径"); return ""; @@ -246,9 +241,7 @@ public class BackgroundSourceUtils { * 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) */ public String getPreviewBackgroundFilePath() { - // 移除:loadSettings(); // 关键修复:避免每次调用都重新加载Bean,导致字段被重置为空 String fileName = previewBackgroundBean.getBackgroundFileName(); - // 强化校验:若文件名为空,返回空路径(避免拼接目录路径) if (TextUtils.isEmpty(fileName)) { LogUtils.e(TAG, "【路径管理】预览背景文件名为空,返回空路径"); return ""; @@ -262,7 +255,6 @@ public class BackgroundSourceUtils { * 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验) */ public String getPreviewBackgroundScaledCompressFilePath() { - // 移除:loadSettings(); // 关键修复 String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); if (TextUtils.isEmpty(compressFileName)) { LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径"); @@ -274,12 +266,12 @@ public class BackgroundSourceUtils { } /** - * 保存配置(JSON文件仍写入应用外置存储) + * 保存配置(核心:将两份独立Bean实例,分别写入各自的JSON文件) */ public void saveSettings() { - BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); - BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); - LogUtils.d(TAG, "【配置管理】配置保存成功:正式Bean=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览Bean=" + previewBackgroundBeanFile.getAbsolutePath()); + BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); // 正式Bean→正式JSON + BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); // 预览Bean→预览JSON + LogUtils.d(TAG, "【配置管理】两份配置保存成功:正式JSON=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览JSON=" + previewBackgroundBeanFile.getAbsolutePath()); } /** @@ -301,73 +293,7 @@ public class BackgroundSourceUtils { return FILE_PROVIDER_AUTHORITY; } - // ------------------------------ 工具方法(适配新目录权限)------------------------------ - /** - * 流复制文件(适配系统公共目录,解决Android14+ 权限问题) - */ - public boolean copyFileByStream(File source, File target) { - if (source == null || !source.exists() || !source.isFile() || target == null) { - LogUtils.e(TAG, "【文件管理】流复制失败:源文件无效或目标文件为空"); - return false; - } - - // 确保目标目录存在(系统公共目录需强制创建,适配/Pictures/PowerBell/路径) - File targetDir = target.getParentFile(); - if (!targetDir.exists()) { - createDirWithPermission(targetDir, "流复制目标目录(/Pictures/PowerBell下)"); - } - - FileInputStream fis = null; - FileOutputStream fos = null; - try { - // 打开源文件输入流(支持共享存储私有文件/系统公共目录文件) - fis = new FileInputStream(source); - // 打开目标文件输出流(适配/Pictures/PowerBell目录权限) - fos = new FileOutputStream(target); - - byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,提升复制效率 - int len; - // 循环读取流并写入目标文件(Java7 兼容语法) - while ((len = fis.read(buffer)) != -1) { - fos.write(buffer, 0, len); - } - - fos.flush(); - fos.getFD().sync(); // 强制同步到磁盘,避免系统公共目录缓存导致文件损坏 - LogUtils.d(TAG, "【文件管理】流复制成功:" + source.getAbsolutePath() + " → " + target.getAbsolutePath() + ",大小:" + target.length() + "bytes"); - // 复制成功后强制设置目标文件权限(确保后续裁剪/预览可读写) - setFilePermissions(target); - return true; - } catch (Exception e) { - LogUtils.e(TAG, "【文件管理】流复制异常(/Pictures/PowerBell目录):" + e.getMessage(), e); - // 复制失败时删除目标文件(避免残留空文件导致后续逻辑异常) - if (target.exists()) { - clearOldFileByExternal(target, "流复制失败残留文件"); - } - // 针对系统公共目录权限异常,给出明确提示 - if (e instanceof SecurityException || e.getMessage().contains("EACCES")) { - ToastUtils.show("图片复制失败,请授予应用「存储权限」和「所有文件访问权限」"); - } - return false; - } finally { - // 关闭流资源(Java7 手动关闭,避免内存泄漏,不依赖try-with-resources) - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【文件管理】源文件流关闭失败:" + e.getMessage()); - } - } - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【文件管理】目标文件流关闭失败:" + e.getMessage()); - } - } - } - } - + // ------------------------------ 核心业务方法(复用FileUtils简化文件操作)------------------------------ /** * 保存图片到预览Bean(图片存储到/Pictures/PowerBell/BackgroundSource,JSON仍存应用外置存储) * @param sourceFile 源图片文件(非空,必须存在) @@ -387,29 +313,30 @@ public class BackgroundSourceUtils { createDirWithPermission(fBackgroundSourceDir, "图片存储目录(saveFileToPreviewBean)"); } - // 生成唯一文件名(基于源文件后缀,避免重复,适配系统公共目录) + // 生成唯一文件名(直接复用FileUtils工具方法,无需重复实现) String uniqueFileName = FileUtils.createUniqueFileName(sourceFile); File previewBackgroundFile = new File(fBackgroundSourceDir, uniqueFileName); - // 核心:用流复制替代原FileUtils.copyFile,适配/Pictures/PowerBell目录权限 - boolean copySuccess = copyFileByStream(sourceFile, previewBackgroundFile); + // 核心:用FileUtils.copyFile替代自定义流复制,精简代码且保证高效 + boolean copySuccess = FileUtils.copyFile(sourceFile, previewBackgroundFile); if (!copySuccess) { LogUtils.e(TAG, "【文件管理】图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + " → " + previewBackgroundFile.getAbsolutePath()); ToastUtils.show("预览图片保存失败"); return previewBackgroundBean; } - // 正确赋值预览Bean(确保文件名非空) - previewBackgroundBean = new BackgroundBean(); + // 预览Bean赋值(仍用独立实例,不影响正式Bean) previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 唯一文件名(非空) + previewBackgroundBean.setBackgroundFilePath(previewBackgroundFile.getAbsolutePath()); // 新增:设置预览原图完整路径 previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_" + previewBackgroundFile.getName()); + previewBackgroundBean.setBackgroundScaledCompressFilePath(fBackgroundSourceDir.getAbsolutePath() + File.separator + "ScaledCompress_" + previewBackgroundFile.getName()); // 新增:设置预览压缩图完整路径 previewBackgroundBean.setBackgroundFileInfo(fileInfo); previewBackgroundBean.setIsUseBackgroundFile(true); - previewBackgroundBean.setIsUseScaledCompress(true); + previewBackgroundBean.setIsUseBackgroundScaledCompressFile(true); // 重命名字段:设置启用压缩图 previewBackgroundBean.setBackgroundWidth(100); previewBackgroundBean.setBackgroundHeight(100); - // 关键强化:强制保存Bean到JSON,确保后续loadSettings()能加载到有效Bean + // 强制保存预览Bean到对应JSON(不影响正式Bean的JSON) BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); LogUtils.d(TAG, "【文件管理】预览Bean强制保存到JSON:" + previewBackgroundBeanFile.getAbsolutePath()); @@ -419,44 +346,53 @@ public class BackgroundSourceUtils { } /** - * 提交预览背景到正式背景(将预览Bean深拷贝到正式Bean,应用预览配置) + * 提交预览背景到正式背景(预览Bean → 正式Bean:深拷贝,新建正式Bean实例+逐字段拷贝) + * 核心:深拷贝后,修改正式Bean不会影响预览Bean,两份实例完全独立 */ public void commitPreviewSourceToCurrent() { - // 深拷贝:新建正式Bean,复制预览Bean的所有字段(避免浅拷贝导致数据污染) + // 深拷贝第一步:新建正式Bean独立实例(彻底脱离预览Bean的引用) currentBackgroundBean = new BackgroundBean(); + // 深拷贝第二步:逐字段拷贝预览Bean的所有值(确保字段无遗漏) currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName()); + currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); // 新增字段:拷贝原图完整路径 currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo()); currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile()); currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName()); - currentBackgroundBean.setIsUseScaledCompress(previewBackgroundBean.isUseScaledCompress()); + currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); // 新增字段:拷贝压缩图完整路径 + currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth()); currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight()); currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor()); - saveSettings(); // 持久化保存正式Bean(JSON写入应用外置存储) - LogUtils.d(TAG, "【配置管理】预览背景提交成功:正式背景更新为/Pictures/PowerBell下的预览背景"); + saveSettings(); // 分别保存:正式Bean→currentJSON,预览Bean→previewJSON(两份独立) + LogUtils.d(TAG, "【配置管理】预览背景深拷贝到正式Bean:两份实例独立,JSON分别存储"); ToastUtils.show("背景图片应用成功"); } /** - * 将正式背景同步到预览背景(深拷贝,初始化预览为当前正式背景) + * 将正式背景同步到预览背景(正式Bean → 预览Bean:深拷贝,新建预览Bean实例+逐字段拷贝) + * 核心:深拷贝后,修改预览Bean不会影响正式Bean,两份实例完全独立 */ public void setCurrentSourceToPreview() { - // 深拷贝:新建预览Bean,复制正式Bean的所有字段(避免浅拷贝导致数据污染) + // 深拷贝第一步:新建预览Bean独立实例(彻底脱离正式Bean的引用) previewBackgroundBean = new BackgroundBean(); + // 深拷贝第二步:逐字段拷贝正式Bean的所有值(确保字段无遗漏) previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName()); + previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); // 新增字段:拷贝原图完整路径 previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo()); previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile()); previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName()); - previewBackgroundBean.setIsUseScaledCompress(currentBackgroundBean.isUseScaledCompress()); + previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); // 新增字段:拷贝压缩图完整路径 + previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth()); previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight()); previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); - saveSettings(); // 持久化保存预览Bean(JSON写入应用外置存储) - LogUtils.d(TAG, "【配置管理】正式背景同步到预览:预览背景更新为/Pictures/PowerBell下的正式背景"); + saveSettings(); // 分别保存:正式Bean→currentJSON,预览Bean→previewJSON(两份独立) + LogUtils.d(TAG, "【配置管理】正式背景深拷贝到预览Bean:两份实例独立,JSON分别存储"); } + // ------------------------------ 必需保留的工具方法(与业务/权限强相关,无法复用FileUtils)------------------------------ /** * 工具方法:创建目录并设置权限(适配系统公共目录/Pictures/PowerBell,确保可读写) * @param dir 要创建的目录 @@ -601,64 +537,6 @@ public class BackgroundSourceUtils { } } - /** - * 工具方法:复制文件(适配大文件,避免OOM,兼容源文件为空场景) - * 【关键优化】适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用 - * @param source 源文件(可为空,为空时仅创建目标目录) - * @param target 目标文件/目录(若源文件为空,target视为目录并创建) - * @return true=复制/创建成功,false=失败 - */ - public boolean copyFile(File source, File target) { - // 场景1:源文件为空 → 仅创建目标目录(适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用) - if (source == null || (source.exists() && source.length() <= 0)) { - if (target == null) { - LogUtils.e(TAG, "【文件管理】目录创建失败:目标目录对象为null"); - return false; - } - // 若target是文件,取其父目录;若本身是目录,直接创建(适配/Pictures/PowerBell目录) - File targetDir = target.isFile() ? target.getParentFile() : target; - createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下)"); - LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath()); - return true; - } - - // 场景2:正常文件复制(源文件非空且存在,适配系统公共目录/Pictures/PowerBell) - if (!source.exists() || target == null) { - LogUtils.e(TAG, "【文件管理】文件复制失败:源文件无效或目标文件为空"); - return false; - } - // 确保目标目录存在(系统公共目录需强制创建,避免权限问题) - File targetDir = target.getParentFile(); - if (!targetDir.exists()) { - createDirWithPermission(targetDir, "文件复制目标目录(/Pictures/PowerBell下)"); - } - // 调用流复制方法(适配系统公共目录权限,避免Permission denied) - return copyFileByStream(source, target); - } - - /** - * 工具方法:清理裁剪相关临时文件(对外提供,Activity退出时调用,适配/Pictures/PowerBell/cache目录) - */ - public void clearCropTempFiles() { - clearOldFile(cropTempFile, "裁剪临时文件(/Pictures/PowerBell/cache)"); - clearOldFile(cropResultFile, "裁剪结果文件(/Pictures/PowerBell/BackgroundSource)"); - // 清理裁剪缓存目录下的其他临时文件(Java7 普通for循环,兼容语法) - if (fPictureCacheDir.exists()) { - File[] files = fPictureCacheDir.listFiles(); - if (files != null && files.length > 0) { - for (int i = 0; i < files.length; i++) { - File file = files[i]; - if (file.isFile()) { - // 强制设置为可写后删除(避免系统公共目录文件只读) - file.setWritable(true, false); - file.delete(); - } - } - } - } - LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成(/Pictures/PowerBell下)"); - } - /** * 对外接口:清理指定旧文件(适配BackgroundSettingsActivity调用,支持/Pictures/PowerBell目录) * @param file 要清理的文件 @@ -668,6 +546,56 @@ public class BackgroundSourceUtils { clearOldFile(file, fileDesc); // 调用内部private方法,复用逻辑 } + /** + * 清理裁剪相关临时文件(对外提供,Activity退出时调用,适配/Pictures/PowerBell/cache目录) + * 【恢复函数】:兼容原代码中 mBgSourceUtils.clearCropTempFiles() 调用 + */ + public void clearCropTempFiles() { + // 清理核心裁剪文件(复用内部clearOldFile方法,逻辑统一) + clearOldFile(cropTempFile, "裁剪临时文件(/Pictures/PowerBell/cache)"); + //clearOldFile(cropResultFile, "裁剪结果文件(/Pictures/PowerBell/BackgroundSource)"); + + // 清理裁剪缓存目录下的所有临时文件(避免残留) + if (fPictureCacheDir.exists()) { + File[] files = fPictureCacheDir.listFiles(); + if (files != null && files.length > 0) { + for (int i = 0; i < files.length; i++) { + File file = files[i]; + if (file.isFile()) { + // 仅清理文件(不删除目录),确保后续裁剪可正常创建文件 + clearOldFile(file, "裁剪缓存目录残留文件"); + } + } + } + } + LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成(/Pictures/PowerBell下)"); + } + + /** + * 适配原调用:mBgSourceUtils.copyFile(new File(""), parentDir) + * 核心:复用FileUtils,支持「空源文件→仅创建目标目录」和「正常文件复制」两种场景 + * @param source 源文件(可为空/空文件,为空时仅创建目标目录) + * @param target 目标文件/目录(若源文件为空,target视为目录并创建) + * @return true=复制/创建成功,false=失败 + */ + public boolean copyFile(File source, File target) { + // 场景1:源文件为空(适配 new File("") 调用)→ 仅创建目标目录 + if (source == null || (source.exists() && source.length() <= 0) || TextUtils.isEmpty(source.getPath())) { + if (target == null) { + LogUtils.e(TAG, "【文件管理】目录创建失败:目标目录对象为null"); + return false; + } + // 若target是文件,取其父目录;若本身是目录,直接创建(适配/Pictures/PowerBell目录) + File targetDir = target.isFile() ? target.getParentFile() : target; + createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下)"); + LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath()); + return true; + } + + // 场景2:正常文件复制(源文件非空且存在)→ 复用FileUtils.copyFile,确保高效兼容 + return FileUtils.copyFile(source, target); + } + /** * 工具方法:获取目录类型描述(用于日志调试,明确目录类型,适配新目录结构) * @param dir 目标目录 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java index 5ef48df7..6cfc56f1 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java @@ -127,7 +127,7 @@ public class BackgroundView extends RelativeLayout { } // 优先加载压缩图,无则加载原图(保持原逻辑) String backgroundPath; - if (previewBean.isUseScaledCompress()) { + if (previewBean.isUseBackgroundScaledCompressFile()) { backgroundPath = backgroundSourceUtils.getPreviewBackgroundScaledCompressFilePath(); } else { backgroundPath = backgroundSourceUtils.getPreviewBackgroundFilePath(); @@ -243,7 +243,7 @@ public class BackgroundView extends RelativeLayout { adjustImageViewSize(); } }); - LogUtils.d(TAG, "父容器未测量完成,延迟调整ImageView尺寸"); + //LogUtils.d(TAG, "父容器未测量完成,延迟调整ImageView尺寸"); return; } diff --git a/powerbell/src/main/res/layout/activity_backgroundpicture.xml b/powerbell/src/main/res/layout/activity_background_settings.xml similarity index 100% rename from powerbell/src/main/res/layout/activity_backgroundpicture.xml rename to powerbell/src/main/res/layout/activity_background_settings.xml