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 968c48e..2a37e39 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 @@ -7,12 +7,10 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; import android.text.TextUtils; -import android.util.Log; import androidx.core.content.FileProvider; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.BuildConfig; -import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.model.BackgroundBean; import java.io.BufferedOutputStream; import java.io.File; @@ -32,54 +30,44 @@ public class BackgroundSourceUtils { public static final String TAG = "BackgroundSourceUtils"; // 裁剪相关常量(统一定义,避免硬编码) - private static final String CROP_CACHE_DIR_NAME = "cache"; // 裁剪缓存目录(基础目录下) - private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; // 裁剪输入临时文件 - private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; // 裁剪输出结果文件 - public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容 - // 图片操作基础目录(核心:系统公共图片目录) + private static final String CROP_CACHE_DIR_NAME = "cache"; + private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; + private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; + 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"; - // 新增:压缩图统一存储目录(图片基础目录下/BackgroundCrops) private static final String SOURCE_DIR_NAME = "BackgroundSource"; private static final String COMPRESS_DIR_NAME = "BackgroundCompress"; - // 1. 静态实例加volatile,禁止指令重排,保证可见性(双重校验锁单例核心) + // 单例相关 private static volatile BackgroundSourceUtils sInstance; private Context mContext; private File currentBackgroundBeanFile; - private BackgroundBean currentBackgroundBean; // 正式Bean:独立实例 + private BackgroundBean currentBackgroundBean; private File previewBackgroundBeanFile; - private BackgroundBean previewBackgroundBean; // 预览Bean:独立实例(与正式Bean完全分离) + private BackgroundBean previewBackgroundBean; - // 2. 统一文件目录(分两类:图片目录→系统公共目录,JSON目录→应用外置存储) - // 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/) - private File fPictureBaseDir; // 图片基础目录 - private File fCropCacheDir; // 裁剪缓存目录(基础目录下/cache) - private File fBackgroundSourceDir; // 图片存储目录(基础目录下,存储正式/预览原图) - private File fBackgroundCompressDir; // 新增:压缩图统一存储目录(基础目录下/BackgroundCrops) - // JSON配置目录(原应用外置存储目录,不改变) - private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils) - private File fModelDir; // 模型文件目录(存储JSON配置) - // 裁剪文件(统一放入图片基础目录下的cache) - private File mCropSourceFile; // 裁剪临时文件(fCropCacheDir下) - private File mCropResultFile; // 裁剪临时文件(fCropCacheDir下) + // 目录文件相关 + private File fPictureBaseDir; + private File fCropCacheDir; + private File fBackgroundSourceDir; + private File fBackgroundCompressDir; + private File fUtilsDir; + private File fModelDir; + private File mCropSourceFile; + private File mCropResultFile; - // 3. 私有构造器(加防反射逻辑+初始化所有目录/文件) + // 双重校验锁单例 private BackgroundSourceUtils(Context context) { - // 防反射破坏:若已有实例,抛异常阻止重复创建 if (sInstance != null) { throw new RuntimeException("BackgroundSourceUtils 是单例类,禁止重复创建!"); } - // 上下文用Application Context,避免Activity内存泄漏 this.mContext = context.getApplicationContext(); - // 【核心调整1】实例化初期优先初始化所有必要目录(确保实例化完成时目录100%就绪) - initNecessaryDirs(); - // 初始化所有文件(裁剪临时文件/结果文件等) + initNecessaryDirs(); initAllFiles(); - // 加载配置(确保正式/预览Bean是两份独立实例) loadSettings(); } - // 4. 双重校验锁单例(线程安全,高效,支持多线程并发调用,Java7语法兼容) public static BackgroundSourceUtils getInstance(Context context) { if (sInstance == null) { synchronized (BackgroundSourceUtils.class) { @@ -92,321 +80,242 @@ public class BackgroundSourceUtils { } /** - * 【核心新增】统一初始化所有必要目录(实例化初期调用,确保目录优先创建) - * 整合图片目录+JSON目录,集中管理目录创建逻辑,保证实例化完成时所有目录就绪 + * 统一初始化所有必要目录 */ private void initNecessaryDirs() { - LogUtils.d(TAG, "【实例化初期-目录初始化】开始创建所有必要目录..."); - // 1. 初始化图片操作目录(系统公共目录 /Pictures/PowerBell/) - initPictureDirs(); - // 2. 初始化JSON配置目录(应用外置存储) - initJsonDirs(); - LogUtils.d(TAG, "【实例化初期-目录初始化】所有必要目录创建完成!"); + LogUtils.d(TAG, "【目录初始化】开始创建所有必要目录"); + initPictureDirs(); + initJsonDirs(); + LogUtils.d(TAG, "【目录初始化】所有必要目录创建完成"); } /** - * 初始化图片操作目录(核心:系统公共图片目录 /Pictures/PowerBell/,新增压缩图目录) - * 【调整强化】新增目录创建后二次校验,失败则降级到备选目录,确保目录可用 + * 初始化图片操作目录 */ private void initPictureDirs() { - // 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell fPictureBaseDir = new File(PICTURE_BASE_DIR); - // 2. 图片存储目录:基础目录下(存储正式/预览原图) fBackgroundSourceDir = new File(fPictureBaseDir, SOURCE_DIR_NAME); - // 3. 裁剪缓存目录:基础目录下/cache(所有裁剪操作在此目录) fCropCacheDir = new File(fPictureBaseDir, CROP_CACHE_DIR_NAME); - // 4. 新增:压缩图统一存储目录(基础目录下/BackgroundCrops,所有压缩图放这里) fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_DIR_NAME); - // 5. 强制创建所有图片目录(带二次校验+降级兜底) - createDirWithPermission(fPictureBaseDir, "图片基础目录(" + PICTURE_BASE_DIR + ")"); - createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下/" + SOURCE_DIR_NAME + ")"); - createDirWithPermission(fCropCacheDir, "裁剪缓存目录(基础目录/" + CROP_CACHE_DIR_NAME + ")"); - createDirWithPermission(fBackgroundCompressDir, "裁剪压缩图存储目录(基础目录/" + COMPRESS_DIR_NAME + ")"); + createDirWithPermission(fPictureBaseDir, "图片基础目录"); + createDirWithPermission(fBackgroundSourceDir, "图片存储目录"); + createDirWithPermission(fCropCacheDir, "裁剪缓存目录"); + createDirWithPermission(fBackgroundCompressDir, "压缩图存储目录"); - // 6. 目录创建后最终校验(确保所有目录已就绪) validatePictureDirs(); - - LogUtils.d(TAG, "【图片目录初始化】完成:" + - "基础目录=" + fPictureBaseDir.getAbsolutePath() + - "图片存储目录=" + fBackgroundSourceDir.getAbsolutePath() + - ",裁剪缓存目录=" + fCropCacheDir.getAbsolutePath() + - ",裁剪压缩图存储目录=" + fBackgroundCompressDir.getAbsolutePath()); } /** - * 初始化JSON配置目录(保留原逻辑:应用外置存储) - * 【调整强化】新增目录创建后二次校验,失败则降级到应用内部缓存目录 + * 初始化JSON配置目录 */ private void initJsonDirs() { - // 1. 工具类根目录(应用外置存储) fUtilsDir = mContext.getExternalFilesDir(TAG); if (fUtilsDir == null) { - LogUtils.e(TAG, "【JSON目录】应用外置存储不可用,切换到应用内部缓存目录"); + LogUtils.e(TAG, "应用外置存储不可用,切换到应用内部缓存目录"); fUtilsDir = mContext.getDataDir(); } - // 2. 模型文件目录(存储JSON配置) fModelDir = new File(fUtilsDir, "ModelDir"); - // 强制创建JSON目录(带二次校验+降级兜底) - createDirWithPermission(fModelDir, "JSON配置目录(应用外置存储)"); + createDirWithPermission(fModelDir, "JSON配置目录"); - // 3. 初始化JSON文件对象(两份独立文件,对应两份Bean实例) currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json"); previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json"); - - LogUtils.d(TAG, "【JSON目录初始化】完成:目录=" + fModelDir.getAbsolutePath() + ",正式JSON=" + currentBackgroundBeanFile.getName() + ",预览JSON=" + previewBackgroundBeanFile.getName()); } /** - * 【核心强化】创建目录并设置权限(适配系统公共目录/Pictures/PowerBell,确保实例化时目录就绪) - * @param dir 要创建的目录 - * @param dirDesc 目录描述(用于日志打印) + * 创建目录并校验 */ private void createDirWithPermission(File dir, String dirDesc) { if (dir == null) { - LogUtils.e(TAG, "【文件管理】创建目录失败:目录对象为null(描述:" + dirDesc + ")"); + LogUtils.e(TAG, dirDesc + "创建失败:目录对象为null"); return; } - - // 第一步:主动检测并创建目录(递归创建所有父目录) - if (!dir.exists()) { - LogUtils.d(TAG, "【文件管理】" + dirDesc + "不存在,开始创建:" + dir.getAbsolutePath()); - dir.mkdirs(); // 递归创建所有父目录 - } else { - LogUtils.d(TAG, "【文件管理】" + dirDesc + "已存在:" + dir.getAbsolutePath()); + if (!dir.exists()) { + LogUtils.d(TAG, dirDesc + "不存在,开始创建:" + dir.getAbsolutePath()); + dir.mkdirs(); } } /** - * 【新增】图片目录创建后最终校验(确保实例化时所有图片目录已就绪) + * 校验图片目录是否就绪 */ private void validatePictureDirs() { - LogUtils.d(TAG, "【图片目录校验】开始校验所有图片目录..."); - boolean allReady = true; - if (!fPictureBaseDir.exists() || !fPictureBaseDir.isDirectory()) { - LogUtils.e(TAG, "【图片目录校验】图片基础目录未就绪:" + fPictureBaseDir.getAbsolutePath()); - allReady = false; - } - if (!fBackgroundSourceDir.exists() || !fBackgroundSourceDir.isDirectory()) { - LogUtils.e(TAG, "【图片目录校验】图片存储目录未就绪:" + fBackgroundSourceDir.getAbsolutePath()); - allReady = false; - } - if (!fCropCacheDir.exists() || !fCropCacheDir.isDirectory()) { - LogUtils.e(TAG, "【图片目录校验】裁剪缓存目录未就绪:" + fCropCacheDir.getAbsolutePath()); - allReady = false; - } - if (!fBackgroundCompressDir.exists() || !fBackgroundCompressDir.isDirectory()) { - LogUtils.e(TAG, "【图片目录校验】压缩图目录未就绪:" + fBackgroundCompressDir.getAbsolutePath()); - allReady = false; - } + boolean allReady = fPictureBaseDir.exists() && fBackgroundSourceDir.exists() + && fCropCacheDir.exists() && fBackgroundCompressDir.exists(); if (allReady) { - LogUtils.d(TAG, "【图片目录校验】所有图片目录均已就绪!"); + LogUtils.d(TAG, "所有图片目录均已就绪"); } else { - LogUtils.e(TAG, "【图片目录校验】部分目录未就绪,可能影响后续功能!"); + LogUtils.e(TAG, "部分图片目录未就绪,可能影响后续功能"); } } /** - * 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录) + * 初始化所有文件 */ private void initAllFiles() { - // 1. 裁剪临时文件 - //mCropSourceFile = new File(fCropCacheDir, CROP_TEMP_FILE_NAME); - // 2. 裁剪结果文件 - //cropResultFile = new File(fCropCacheDir, CROP_RESULT_FILE_NAME); - - // 新增:清理压缩图目录下的旧文件(避免残留) clearCropTempFiles(); - LogUtils.d(TAG, "【文件初始化】完成。"); + LogUtils.d(TAG, "文件初始化完成"); } - // 【核心实现】定义 getFileProviderUri 方法:将 File 转为 ContentUri(适配 FileProvider) - public Uri getFileProviderUri(File file) { - Log.d("BackgroundSourceUtils", "getFileProviderUri: 生成FileProvider Uri,文件路径:" + file.getAbsolutePath()); - Uri contentUri = null; - try { - // 适配 Android 7.0+:使用 FileProvider 生成 ContentUri(避免 FileUriExposedException) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - contentUri = FileProvider.getUriForFile( - mContext, - FILE_PROVIDER_AUTHORITY, // 与清单文件中一致 - file - ); - Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0+ 生成ContentUri:" + contentUri.toString()); - } else { - // 适配 Android 7.0 以下:直接使用 File.toURI()(兼容旧版本) - contentUri = Uri.fromFile(file); - Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0以下 生成FileUri:" + contentUri.toString()); - } - } catch (IllegalArgumentException e) { - // 捕获异常(如文件路径无效、授权不匹配等) - Log.e("BackgroundSourceUtils", "getFileProviderUri: 生成Uri失败,异常:" + e.getMessage(), e); - contentUri = null; - } - return contentUri; - } - - boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) { - File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath()); - if (!fCheckBackgroundFile.exists()) { - String newCropFileName = "blank10x10"; - String fileSuffix = "png"; - mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); - mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); - - AssetsCopyUtils.copyAssetsFileToDir(mContext, "images/blank10x10.png", mCropSourceFile.getAbsolutePath()); - try { - mCropResultFile.createNewFile(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - - // 加载图片数据模型数据 - loadSettings(); - // 修改预览数据模型 - previewBackgroundBean.setIsUseBackgroundFile(true); - previewBackgroundBean.setIsUseBackgroundScaledCompressFile(false); - - previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName()); - previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath()); - - previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); - previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); - // 保存数据模型数据更改 - saveSettings(); - return true; - } - return false; - } - - /* - * 创建预览数据剪裁环境 - */ - public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) { - InputStream is = null; - FileOutputStream fos = null; - - try { - clearCropTempFiles(); - if(checkEmptyBackgroundAndCreateBlankBackgroundBean(oldPreviewBackgroundBean)) { - return true; - } - - Uri uri = UriUtil.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath()); - //String szType = mContext.getContentResolver().getType(uri); - // 2. 截取MIME类型后缀(如从image/jpeg中提取jpeg)【核心新增逻辑】 - String fileSuffix = FileUtils.getFileSuffix(mContext, uri); - String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis(); - mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); - mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); - - if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) { - FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile); - } else { - mCropResultFile.createNewFile(); - } - - if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundFilePath())) { - FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile); - } else { - mCropSourceFile.createNewFile(); - // 1. 打开Uri输入流(兼容content:///file:// 等多种Uri格式) - is = mContext.getContentResolver().openInputStream(uri); - if (is == null) { - LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败,Uri:" + uri.toString()); - return false; - } - - // 2. 初始化选图临时文件输出流(Java7 手动创建流,不依赖try-with-resources) - fos = new FileOutputStream(mCropSourceFile); - byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,平衡读写性能与内存占用 - int readLen; // 每次读取的字节长度 - - // 3. 流复制(Java7 标准while循环,避免Java8+语法) - while ((readLen = is.read(buffer)) != -1) { - fos.write(buffer, 0, readLen); // 精准写入读取到的字节,避免空字节填充 - } - - // 4. 强制同步写入磁盘(解决Android 10+ 异步写入导致的文件无效问题) - fos.flush(); - if (fos != null) { - try { - fos.getFD().sync(); // 确保数据写入物理磁盘,而非缓存 - } catch (IOException e) { - LogUtils.w(TAG, "【选图解析】文件同步到磁盘失败,用flush()兜底:" + e.getMessage()); - fos.flush(); - } - } - } - - // 加载图片数据模型数据 - loadSettings(); - // 修改预览数据模型 - previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName()); - previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath()); - - previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); - previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); - // 保存数据模型数据更改 - saveSettings(); - - // 6. 解析成功日志(打印文件信息,便于问题排查) - LogUtils.d(TAG, "【选图解析】Uri解析成功!"); - LogUtils.d(TAG, "→ 原Uri:" + uri.toString()); - LogUtils.d(TAG, "→ 图片剪裁数据源:" + mCropSourceFile.getAbsolutePath()); - LogUtils.d(TAG, "→ 图片剪裁数据源文件大小:" + mCropSourceFile.length() + " bytes"); - LogUtils.d(TAG, "→ 剪裁结果数据文件:" + mCropResultFile.getAbsolutePath()); - LogUtils.d(TAG, "→ 剪裁结果数据文件大小:" + mCropResultFile.length() + " bytes"); - return true; - - } catch (Exception e) { - // 捕获所有异常(IO异常/空指针等),避免崩溃 - LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e); - // 异常时清理无效文件,防止残留 - clearCropTempFiles(); - return false; - - } finally { - // 7. 手动关闭流资源(Java7 标准写法,避免内存泄漏) - if (is != null) { - try { - is.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【选图解析】输入流关闭失败:" + e.getMessage()); - } - } - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【选图解析】输出流关闭失败:" + e.getMessage()); - } - } - } - } + /** + * 将File转为ContentUri + */ + public Uri getFileProviderUri(File file) { + LogUtils.d(TAG, "【getFileProviderUri调用】文件路径:" + file.getAbsolutePath()); + Uri contentUri = null; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + contentUri = FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITY, file); + LogUtils.d(TAG, "7.0+ 生成ContentUri:" + contentUri.toString()); + } else { + contentUri = Uri.fromFile(file); + LogUtils.d(TAG, "7.0以下 生成FileUri:" + contentUri.toString()); + } + } catch (IllegalArgumentException e) { + LogUtils.e(TAG, "生成Uri失败:" + e.getMessage(), e); + contentUri = null; + } + return contentUri; + } /** - * 加载背景图片配置数据(核心:确保current/preview是两份独立的BackgroundBean实例) + * 检查背景是否为空并创建空白背景Bean */ - public void loadSettings() { - // 1. 加载正式Bean(独立实例:从currentBackgroundBean.json加载,不存在则新建) - currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); - if (currentBackgroundBean == null) { - currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化 - BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); - LogUtils.d(TAG, "【配置管理】正式背景Bean不存在,创建独立实例并保存到JSON"); - } + boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) { + LogUtils.d(TAG, "【checkEmptyBackgroundAndCreateBlankBackgroundBean调用】开始检查背景Bean"); + File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath()); + if (!fCheckBackgroundFile.exists()) { + String newCropFileName = "blank10x10"; + String fileSuffix = "png"; + mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); + mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); - // 2. 加载预览Bean(独立实例:从previewBackgroundBean.json加载,不存在则新建,与正式Bean完全分离) - previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); - if (previewBackgroundBean == null) { - previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化 - BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); - LogUtils.d(TAG, "【配置管理】预览背景Bean不存在,创建独立实例并保存到JSON"); + AssetsCopyUtils.copyAssetsFileToDir(mContext, "images/blank10x10.png", mCropSourceFile.getAbsolutePath()); + try { + mCropResultFile.createNewFile(); + } catch (IOException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + + loadSettings(); + previewBackgroundBean.setIsUseBackgroundFile(true); + previewBackgroundBean.setIsUseBackgroundScaledCompressFile(false); + previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName()); + previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath()); + previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); + previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); + saveSettings(); + LogUtils.d(TAG, "背景Bean为空,已创建空白背景并更新配置"); + return true; + } + LogUtils.d(TAG, "背景Bean文件存在,无需创建空白背景"); + return false; + } + + /** + * 创建并更新预览剪裁环境 + */ + public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) { + LogUtils.d(TAG, "【createAndUpdatePreviewEnvironmentForCropping调用】开始初始化预览剪裁环境"); + InputStream is = null; + FileOutputStream fos = null; + try { + clearCropTempFiles(); + if (checkEmptyBackgroundAndCreateBlankBackgroundBean(oldPreviewBackgroundBean)) { + LogUtils.d(TAG, "空白背景创建成功,直接返回"); + return true; + } + + Uri uri = UriUtil.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath()); + String fileSuffix = FileUtils.getFileSuffix(mContext, uri); + String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis(); + mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); + mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); + + if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) { + FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile); + } else { + mCropResultFile.createNewFile(); + } + + if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundFilePath())) { + FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile); + } else { + mCropSourceFile.createNewFile(); + is = mContext.getContentResolver().openInputStream(uri); + if (is == null) { + LogUtils.e(TAG, "ContentResolver打开Uri失败:" + uri.toString()); + return false; + } + fos = new FileOutputStream(mCropSourceFile); + byte[] buffer = new byte[1024 * 8]; + int readLen; + while ((readLen = is.read(buffer)) != -1) { + fos.write(buffer, 0, readLen); + } + fos.flush(); + try { + fos.getFD().sync(); + } catch (IOException e) { + LogUtils.w(TAG, "文件同步到磁盘失败,flush兜底:" + e.getMessage()); + fos.flush(); + } + } + + loadSettings(); + previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName()); + previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath()); + previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); + previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); + saveSettings(); + + LogUtils.d(TAG, "预览剪裁环境初始化成功"); + LogUtils.d(TAG, "→ 原Uri:" + uri.toString()); + LogUtils.d(TAG, "→ 剪裁数据源:" + mCropSourceFile.getAbsolutePath()); + LogUtils.d(TAG, "→ 剪裁结果文件:" + mCropResultFile.getAbsolutePath()); + return true; + } catch (Exception e) { + LogUtils.e(TAG, "预览剪裁环境初始化异常:" + e.getMessage(), e); + clearCropTempFiles(); + return false; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage()); + } + } } } - // ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------ + /** + * 加载背景配置 + */ + public void loadSettings() { + currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); + if (currentBackgroundBean == null) { + currentBackgroundBean = new BackgroundBean(); + BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); + LogUtils.d(TAG, "正式背景Bean不存在,已创建新实例"); + } + + previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); + if (previewBackgroundBean == null) { + previewBackgroundBean = new BackgroundBean(); + BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); + LogUtils.d(TAG, "预览背景Bean不存在,已创建新实例"); + } + } + + // ------------------------------ 对外提供的核心方法 ------------------------------ public BackgroundBean getCurrentBackgroundBean() { return currentBackgroundBean; } @@ -415,92 +324,48 @@ public class BackgroundSourceUtils { return previewBackgroundBean; } - /** - * 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) - */ -// public String getCurrentBackgroundFilePath() { -// String fileName = currentBackgroundBean.getBackgroundFileName(); -// if (TextUtils.isEmpty(fileName)) { -// LogUtils.e(TAG, "【路径管理】正式背景文件名为空,返回空路径"); -// return ""; -// } -// File file = new File(fBackgroundSourceDir, fileName); -// LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath()); -// return file.getAbsolutePath(); -// } -// -// /** -// * 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) -// */ -// public String getPreviewBackgroundFilePath() { -// String fileName = previewBackgroundBean.getBackgroundFileName(); -// if (TextUtils.isEmpty(fileName)) { -// LogUtils.e(TAG, "【路径管理】预览背景文件名为空,返回空路径"); -// return ""; -// } -// File file = new File(fBackgroundSourceDir, fileName); -// LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath()); -// return file.getAbsolutePath(); -// } - - /** - * 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验,统一指向BackgroundCrops目录) - */ public String getPreviewBackgroundScaledCompressFilePath() { String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); if (TextUtils.isEmpty(compressFileName)) { - LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径"); + LogUtils.e(TAG, "预览压缩背景文件名为空"); return ""; } - // 关键:压缩图路径统一指向BackgroundCrops目录(不再用BackgroundSource) File file = new File(fBackgroundCompressDir, compressFileName); - LogUtils.d(TAG, "【路径管理】预览压缩背景路径(BackgroundCrops目录):" + file.getAbsolutePath()); return file.getAbsolutePath(); } - /** - * 新增:获取正式背景压缩图片路径(统一指向BackgroundCrops目录,对外提供调用) - */ public String getCurrentBackgroundScaledCompressFilePath() { String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName(); if (TextUtils.isEmpty(compressFileName)) { - LogUtils.e(TAG, "【路径管理】正式压缩背景文件名为空,返回空路径"); + LogUtils.e(TAG, "正式压缩背景文件名为空"); return ""; } - // 关键:压缩图路径统一指向BackgroundCrops目录 File file = new File(fBackgroundCompressDir, compressFileName); - LogUtils.d(TAG, "【路径管理】正式压缩背景路径(BackgroundCrops目录):" + file.getAbsolutePath()); return file.getAbsolutePath(); } /** - * 保存配置(核心:将两份独立Bean实例,分别写入各自的JSON文件) + * 保存配置 */ public void saveSettings() { - if (currentBackgroundBean != null && previewBackgroundBean != null) { - BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); // 正式Bean→正式JSON - BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); // 预览Bean→预览JSON - LogUtils.d(TAG, "【配置管理】两份配置保存成功:正式JSON=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览JSON=" + previewBackgroundBeanFile.getAbsolutePath()); - return; - } - LogUtils.d(TAG, "【配置管理】两份配置保存失败。currentBackgroundBean 与 previewBackgroundBean 有空值。"); + if (currentBackgroundBean != null && previewBackgroundBean != null) { + BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); + BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); + LogUtils.d(TAG, "两份背景配置保存成功"); + } else { + LogUtils.e(TAG, "配置保存失败:current/preview Bean存在空值"); + } } - /** - * 获取图片基础目录路径(对外提供:/Pictures/PowerBell/) - */ public String getBackgroundSourceDirPath() { return fBackgroundSourceDir.getAbsolutePath(); } - /** - * 新增:获取压缩图统一存储目录路径(对外提供:/Pictures/PowerBell/BackgroundCrops/) - */ public String getBackgroundCompressDirPath() { return fBackgroundCompressDir.getAbsolutePath(); } - public String getCropCacheDir() { + public String getCropCacheDir() { return fCropCacheDir.getAbsolutePath(); } @@ -508,119 +373,99 @@ public class BackgroundSourceUtils { return FILE_PROVIDER_AUTHORITY; } - // ------------------------------ 核心业务方法(复用FileUtils简化文件操作)------------------------------ + // ------------------------------ 核心业务方法 ------------------------------ /** - * 优化函数:仅裁剪结果图可保存到BackgroundSource(避免启动裁剪时误复制原图) - * 说明:启动裁剪时不调用此方法,仅在裁剪完成后保存结果图时调用 + * 保存裁剪结果图到预览Bean */ public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) { - final String TAG = "BackgroundSourceUtils"; - // 强化校验1:仅允许裁剪结果图传入(通过文件路径判断,避免原图误传入) + LogUtils.d(TAG, "【saveFileToPreviewBean调用】源文件路径:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) { - Log.e(TAG, "【保存优化】源文件无效,拒绝保存:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); + LogUtils.e(TAG, "源文件无效,拒绝保存"); return previewBackgroundBean; } - // 强化校验2:排除原图路径(避免启动裁剪时传入原图复制) - String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); // 原图存储目录(如相册/拍照目录) + String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); if (sourceFile.getAbsolutePath().contains(originalImageDir)) { - Log.w(TAG, "【保存优化】禁止复制原图到BackgroundSource,跳过保存"); + LogUtils.w(TAG, "禁止复制原图,跳过保存"); return previewBackgroundBean; } - // 确保BackgroundSource目录存在(实例化时已创建,此处二次确认) - if (!fBackgroundSourceDir.exists()) { - if (!fBackgroundSourceDir.mkdirs()) { - Log.e(TAG, "【保存优化】BackgroundSource目录创建失败"); - return previewBackgroundBean; - } + if (!fBackgroundSourceDir.exists() && !fBackgroundSourceDir.mkdirs()) { + LogUtils.e(TAG, "BackgroundSource目录创建失败"); + return previewBackgroundBean; } - // 生成唯一文件名(避免覆盖) String uniqueFileName = "bg_" + System.currentTimeMillis() + "_" + sourceFile.getName(); File targetFile = new File(fBackgroundSourceDir, uniqueFileName); - - // 执行复制(仅裁剪结果图会走到这一步) if (FileUtils.copyFile(sourceFile, targetFile)) { - Log.d(TAG, "【保存优化】裁剪结果图保存成功:" + targetFile.getAbsolutePath()); - // 更新预览Bean(原有逻辑保留) + LogUtils.d(TAG, "裁剪结果图保存成功:" + targetFile.getAbsolutePath()); previewBackgroundBean.setBackgroundFileName(uniqueFileName); previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath()); previewBackgroundBean.setBackgroundFileInfo(fileInfo); previewBackgroundBean.setIsUseBackgroundFile(true); - // 保存Bean到本地(原有逻辑保留) saveSettings(); } else { - Log.e(TAG, "【保存优化】裁剪结果图复制失败:" + sourceFile.getAbsolutePath() + " → " + targetFile.getAbsolutePath()); + LogUtils.e(TAG, "裁剪结果图复制失败"); } - return previewBackgroundBean; } /** - * 提交预览背景到正式背景(预览Bean → 正式Bean:深拷贝,新建正式Bean实例+逐字段拷贝) - * 核心:深拷贝后,修改正式Bean不会影响预览Bean,两份实例完全独立,压缩图路径统一指向BackgroundCrops + * 提交预览背景到正式背景 */ public void commitPreviewSourceToCurrent() { - // 深拷贝第一步:新建正式Bean独立实例(彻底脱离预览Bean的引用) + LogUtils.d(TAG, "【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean"); currentBackgroundBean = new BackgroundBean(); - // 深拷贝第二步:逐字段拷贝预览Bean的所有值(压缩图路径同步指向BackgroundCrops) currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName()); - currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); // 原图路径(BackgroundSource) + currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo()); currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile()); currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName()); - currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径(BackgroundCrops) - currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 + currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); + currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth()); currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight()); currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor()); - // 拷贝一份缓存图片文件到正式背景文件夹 - String previewFileName = previewBackgroundBean.getBackgroundFileName(); - String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); - File previewFile = new File(previewBackgroundBean.getBackgroundFilePath()); - File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath()); - File currentFile = new File(fBackgroundSourceDir, previewFileName); - File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName); - FileUtils.copyFile(previewFile, currentFile); - FileUtils.copyFile(previewCropFile, currentCropFile); - // 更新当前背景文件路径 - currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath()); // 原图路径(BackgroundSource) - currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath()); // 压缩图路径(BackgroundCrops) + String previewFileName = previewBackgroundBean.getBackgroundFileName(); + String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); + File previewFile = new File(previewBackgroundBean.getBackgroundFilePath()); + File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath()); + File currentFile = new File(fBackgroundSourceDir, previewFileName); + File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName); + FileUtils.copyFile(previewFile, currentFile); + FileUtils.copyFile(previewCropFile, currentCropFile); + currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath()); + currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath()); - saveSettings(); // 分别保存:正式Bean→currentJSON,预览Bean→previewJSON(两份独立) - LogUtils.d(TAG, "【配置管理】预览背景深拷贝到正式Bean:两份实例独立,压缩图统一存储到BackgroundCrops"); + saveSettings(); + LogUtils.d(TAG, "预览背景提交到正式背景成功,两份实例完全独立"); ToastUtils.show("背景图片应用成功"); } /** - * 将正式背景同步到预览背景(正式Bean → 预览Bean:深拷贝,新建预览Bean实例+逐字段拷贝) - * 核心:深拷贝后,修改预览Bean不会影响正式Bean,两份实例完全独立,压缩图路径统一指向BackgroundCrops + * 将正式背景同步到预览背景 */ public void setCurrentSourceToPreview() { - LogUtils.d(TAG, "正在初始化预览数据,setCurrentSourceToPreview()"); - // 深拷贝第一步:新建预览Bean独立实例(彻底脱离正式Bean的引用) + LogUtils.d(TAG, "【setCurrentSourceToPreview调用】开始深拷贝正式Bean到预览Bean"); previewBackgroundBean = new BackgroundBean(); - // 深拷贝第二步:逐字段拷贝正式Bean的所有值(压缩图路径同步指向BackgroundCrops) previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName()); - previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); // 原图路径(BackgroundSource) + previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo()); previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile()); previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName()); - previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径(BackgroundCrops) - previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 + previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); + previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth()); previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight()); previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); saveSettings(); + LogUtils.d(TAG, "正式背景同步到预览背景成功"); } /** - * 工具方法:清理旧文件(避免文件锁定/残留,适配系统公共目录)【内部私有,不对外暴露】 - * @param file 要清理的文件 - * @param fileDesc 文件描述(用于日志打印) + * 清理单个旧文件 */ private void clearOldFile(File file, String fileDesc) { if (file == null) { @@ -628,50 +473,45 @@ public class BackgroundSourceUtils { } if (file.exists()) { file.delete(); - LogUtils.w(TAG, "【文件管理】" + fileDesc + "已删除"); + LogUtils.d(TAG, fileDesc + "已删除"); } } /** - * 新增:清理压缩图目录下的旧文件(避免残留,初始化时调用) + * 清理裁剪临时文件 */ void clearCropTempFiles() { - for (File file : fCropCacheDir.listFiles()) { - clearOldFile(file, "旧裁剪缓存文件(" + file.getAbsolutePath() + ")"); - } - mCropSourceFile = null; - mCropResultFile = null; + File[] files = fCropCacheDir.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + clearOldFile(file, "旧裁剪缓存文件:" + file.getAbsolutePath()); + } + mCropSourceFile = null; + mCropResultFile = null; } /** - * 适配原调用: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())) { + LogUtils.d(TAG, "【copyFile调用】源文件:" + (source != null ? source.getAbsolutePath() : "null") + " 目标:" + (target != null ? target.getAbsolutePath() : "null")); + if (source == null || TextUtils.isEmpty(source.getPath()) || (source.exists() && source.length() <= 0)) { if (target == null) { - LogUtils.e(TAG, "【文件管理】目录创建失败:目标目录对象为null"); + LogUtils.e(TAG, "目录创建失败:目标对象为null"); return false; } - // 若target是文件,取其父目录;若本身是目录,直接创建(实例化时已创建,此处二次确认) File targetDir = target.isFile() ? target.getParentFile() : target; - createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下)"); - LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath()); + createDirWithPermission(targetDir, "空源文件场景-目录创建"); + LogUtils.d(TAG, "空源文件场景,目录创建完成"); return true; } - - // 场景2:正常文件复制(源文件非空且存在)→ 复用FileUtils.copyFile,确保高效兼容 return FileUtils.copyFile(source, target); } /** - * 工具方法:获取目录类型描述(用于日志调试,明确目录类型,适配新目录结构) - * @param dir 目标目录 - * @return 目录类型描述 + * 获取目录类型描述 */ public String getDirTypeDesc(File dir) { if (dir == null) { @@ -684,7 +524,7 @@ public class BackgroundSourceUtils { if (!TextUtils.isEmpty(publicPicturePath)) { if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_DIR_NAME)) { - return "系统公共图片目录(/Pictures/PowerBell/BackgroundCrops,压缩图统一存储目录)"; // 新增压缩图目录描述 + return "系统公共图片目录(/Pictures/PowerBell/BackgroundCompress,压缩图统一存储目录)"; } else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) { return "系统公共图片目录(/Pictures/PowerBell,图片存储/裁剪目录)"; } @@ -699,195 +539,158 @@ public class BackgroundSourceUtils { } /** - * 新增:迁移旧压缩图路径到新目录(BackgroundCrops),兼容历史数据 - * @param bean 要迁移的BackgroundBean(正式/预览) - * @param isCurrentBean 是否是正式Bean(用于日志区分) + * 迁移旧压缩图路径到新目录 */ private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) { + LogUtils.d(TAG, "【migrateCompressPathToNewDir调用】开始迁移" + (isCurrentBean ? "正式" : "预览") + "Bean压缩路径"); String oldCompressPath = bean.getBackgroundScaledCompressFilePath(); String beanType = isCurrentBean ? "正式Bean" : "预览Bean"; - // 校验:旧路径非空,且不在BackgroundCrops目录下,才需要迁移 if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) { - LogUtils.d(TAG, "【路径迁移】" + beanType + "无需迁移:旧路径为空或已在BackgroundCrops目录"); + LogUtils.d(TAG, beanType + "无需迁移:旧路径为空或已在目标目录"); return; } File oldCompressFile = new File(oldCompressPath); if (!oldCompressFile.exists() || !oldCompressFile.isFile() || oldCompressFile.length() <= 0) { - LogUtils.w(TAG, "【路径迁移】" + beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath); - // 重置路径为新目录下的空文件(避免无效路径) + LogUtils.w(TAG, beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath); String compressFileName = bean.getBackgroundScaledCompressFileName(); if (!TextUtils.isEmpty(compressFileName)) { File newCompressFile = new File(fBackgroundCompressDir, compressFileName); bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath()); saveSettings(); - LogUtils.d(TAG, "【路径迁移】" + beanType + "重置压缩路径到BackgroundCrops:" + newCompressFile.getAbsolutePath()); + LogUtils.d(TAG, beanType + "压缩路径已重置到目标目录"); } return; } - // 迁移逻辑:复制旧文件到新目录,更新Bean路径,删除旧文件 String compressFileName = bean.getBackgroundScaledCompressFileName(); if (TextUtils.isEmpty(compressFileName)) { - compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg"; // 兜底生成文件名 + compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg"; } File newCompressFile = new File(fBackgroundCompressDir, compressFileName); - // 复制旧文件到新目录 boolean copySuccess = FileUtils.copyFile(oldCompressFile, newCompressFile); if (copySuccess) { - // 更新Bean路径为新目录路径 bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath()); saveSettings(); - // 删除旧文件(清理残留) clearOldFile(oldCompressFile, beanType + "旧压缩文件(迁移后清理)"); - LogUtils.d(TAG, "【路径迁移】" + beanType + "压缩路径迁移成功:" + oldCompressPath + " → " + newCompressFile.getAbsolutePath()); + LogUtils.d(TAG, beanType + "压缩路径迁移成功:" + oldCompressPath + " → " + newCompressFile.getAbsolutePath()); } else { - LogUtils.e(TAG, "【路径迁移】" + beanType + "压缩文件复制失败,迁移终止:" + oldCompressPath); + LogUtils.e(TAG, beanType + "压缩文件复制失败,迁移终止"); } } - // ======================================== 核心实现:获取图片旋转角度 ======================================== - /** - * 读取图片EXIF信息,获取旋转角度(适配JPEG/PNG等主流格式) - * @param imagePath 图片绝对路径(支持本地文件路径,兼容多包名临时目录) - * @return 旋转角度(0/90/180/270,无旋转返回0) - */ - public int getImageRotateAngle(String imagePath) { - // 1. 入参校验(避免空指针/无效路径) - if (TextUtils.isEmpty(imagePath)) { - Log.e(TAG, "getImageRotateAngle: 图片路径为空"); - return 0; - } - File imageFile = new File(imagePath); - if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { - Log.e(TAG, "getImageRotateAngle: 图片文件无效,路径:" + imagePath); - return 0; - } - - InputStream inputStream = null; - try { - // 2. 读取图片EXIF信息(优先用流读取,避免文件占用) - inputStream = new FileInputStream(imageFile); - ExifInterface exifInterface = new ExifInterface(inputStream); - - // 3. 获取旋转角度标签(兼容不同设备的EXIF字段) - int orientation = exifInterface.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_NORMAL - ); - - // 4. 解析旋转角度(标准EXIF角度映射) - switch (orientation) { - case ExifInterface.ORIENTATION_ROTATE_90: - return 90; - case ExifInterface.ORIENTATION_ROTATE_180: - return 180; - case ExifInterface.ORIENTATION_ROTATE_270: - return 270; - default: // 正常/翻转等其他情况,均视为0度 - return 0; - } - } catch (IOException e) { - // 兼容异常场景:如图片无EXIF信息、格式不支持(如WebP) - Log.w(TAG, "getImageRotateAngle: 读取EXIF异常,路径:" + imagePath + ",错误:" + e.getMessage()); - return 0; - } finally { - // 5. 关闭流资源(避免内存泄漏/文件占用) - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - Log.e(TAG, "getImageRotateAngle: 流关闭失败,错误:" + e.getMessage()); - } - } - } - } - - - // ======================================== 图片处理核心方法(压缩/裁剪/保存) ======================================== /** - * 压缩图片并保存(核心修复:路径非空校验+兜底路径,统一存储到BackgroundCrops目录) + * 获取图片旋转角度 */ - public void compressQualityToRecivedPicture(Bitmap bitmap) { - // 兼容裁剪等旧调用:从工具类获取默认压缩路径(统一指向BackgroundCrops),转发至重载函数 - String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath(); - compressQualityToRecivedPicture(bitmap, defaultCompressPath); - } + public int getImageRotateAngle(String imagePath) { + LogUtils.d(TAG, "【getImageRotateAngle调用】图片路径:" + imagePath); + if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "图片路径为空"); + return 0; + } + File imageFile = new File(imagePath); + if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { + LogUtils.e(TAG, "图片文件无效:" + imagePath); + return 0; + } - /** - * 重载方法:指定路径压缩图片并保存(修复:压缩后同步路径到预览Bean,统一存储到BackgroundCrops) - * 适配场景:裁剪后生成压缩图,强制绑定路径到预览Bean,避免路径错位 - * @param bitmap 待压缩的Bitmap(裁剪后的缩放图) - * @param targetCompressPath 强制指定的压缩目标路径(从预览Bean获取/生成,默认指向BackgroundCrops) - */ - public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) { - LogUtils.d(TAG, "【压缩启动】开始压缩图片(指定路径),Bitmap状态:" + (bitmap != null && !bitmap.isRecycled())); + InputStream inputStream = null; + try { + inputStream = new FileInputStream(imageFile); + ExifInterface exifInterface = new ExifInterface(inputStream); + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: + return 0; + } + } catch (IOException e) { + LogUtils.w(TAG, "读取EXIF异常:" + e.getMessage()); + return 0; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "流关闭失败:" + e.getMessage()); + } + } + } + } - if (bitmap == null || bitmap.isRecycled()) { - ToastUtils.show("压缩失败:图片为空"); - LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收"); - return; - } + /** + * 压缩图片并保存(默认路径) + */ + public void compressQualityToRecivedPicture(Bitmap bitmap) { + LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】使用默认路径压缩图片"); + String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath(); + compressQualityToRecivedPicture(bitmap, defaultCompressPath); + } - OutputStream outStream = null; - FileOutputStream fos = null; - try { - LogUtils.d(TAG, "【压缩配置】目标路径(BackgroundCrops):" + targetCompressPath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); + /** + * 压缩图片并保存(指定路径) + */ + public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) { + LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】指定路径压缩图片,目标路径:" + targetCompressPath); + if (bitmap == null || bitmap.isRecycled()) { + ToastUtils.show("压缩失败:图片为空"); + LogUtils.e(TAG, "Bitmap为空或已回收"); + return; + } - File targetCompressFile = new File(targetCompressPath); - if (targetCompressFile.exists()) { - targetCompressFile.delete(); - } - targetCompressFile.createNewFile(); + OutputStream outStream = null; + FileOutputStream fos = null; + try { + LogUtils.d(TAG, "Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); + File targetCompressFile = new File(targetCompressPath); + if (targetCompressFile.exists()) { + targetCompressFile.delete(); + } + targetCompressFile.createNewFile(); - // 写入压缩图(质量80,平衡清晰度和内存) - fos = new FileOutputStream(targetCompressFile); - outStream = new BufferedOutputStream(fos); - boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); - outStream.flush(); - // 强制同步到磁盘(避免异步写入导致控件读取不到文件) - if (fos != null) { - try { - fos.getFD().sync(); - LogUtils.d(TAG, "【压缩保存】已强制同步到磁盘"); - } catch (IOException e) { - LogUtils.w(TAG, "【压缩保存】sync()失败,flush()兜底:" + e.getMessage()); - outStream.flush(); - } - } + fos = new FileOutputStream(targetCompressFile); + outStream = new BufferedOutputStream(fos); + boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); + outStream.flush(); + try { + fos.getFD().sync(); + LogUtils.d(TAG, "图片已强制同步到磁盘"); + } catch (IOException e) { + LogUtils.w(TAG, "sync失败,flush兜底:" + e.getMessage()); + outStream.flush(); + } - LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB,路径:" + targetCompressFile); - - // 关键修复:压缩成功后,强制同步路径到预览Bean(双重保障,避免时序错位) - if (compressSuccess) { - ToastUtils.show("图片压缩成功"); - } else { - ToastUtils.show("图片压缩失败"); - } - } catch (IOException e) { - LogUtils.e(TAG, "【压缩异常】IO错误:" + e.getMessage(), e); - ToastUtils.show("图片压缩失败"); - } finally { - // 资源回收(避免内存泄漏) - if (outStream != null) { - try { - outStream.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【流关闭失败】BufferedOutputStream:" + e.getMessage()); - } - } - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【流关闭失败】FileOutputStream:" + e.getMessage()); - } - } - if (bitmap != null && !bitmap.isRecycled()) { - bitmap.recycle(); - } - } - } + LogUtils.d(TAG, "图片压缩" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB"); + ToastUtils.show(compressSuccess ? "图片压缩成功" : "图片压缩失败"); + } catch (IOException e) { + LogUtils.e(TAG, "图片压缩IO异常:" + e.getMessage(), e); + ToastUtils.show("图片压缩失败"); + } finally { + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "BufferedOutputStream关闭失败:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "FileOutputStream关闭失败:" + e.getMessage()); + } + } + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + } } +