20251202_022447_396

This commit is contained in:
2025-12-02 02:24:50 +08:00
parent 5d3d46f2fe
commit f6a00fac36
3 changed files with 1314 additions and 852 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Mon Dec 01 16:33:23 GMT 2025 #Mon Dec 01 18:24:07 GMT 2025
stageCount=13 stageCount=13
libraryProject= libraryProject=
baseVersion=15.11 baseVersion=15.11
publishVersion=15.11.12 publishVersion=15.11.12
buildCount=98 buildCount=102
baseBetaVersion=15.11.13 baseBetaVersion=15.11.13

View File

@@ -34,6 +34,8 @@ public class BackgroundSourceUtils {
public 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"; private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell";
// 新增:压缩图统一存储目录(图片基础目录下/BackgroundCrops
private static final String COMPRESS_BASE_DIR_NAME = "BackgroundCrops";
// 1. 静态实例加volatile禁止指令重排保证可见性双重校验锁单例核心 // 1. 静态实例加volatile禁止指令重排保证可见性双重校验锁单例核心
private static volatile BackgroundSourceUtils sInstance; private static volatile BackgroundSourceUtils sInstance;
@@ -47,7 +49,8 @@ public class BackgroundSourceUtils {
// 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/ // 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/
private File fPictureBaseDir; // 图片基础目录 private File fPictureBaseDir; // 图片基础目录
private File fPictureCacheDir; // 裁剪缓存目录(基础目录下/cache private File fPictureCacheDir; // 裁剪缓存目录(基础目录下/cache
private File fBackgroundSourceDir; // 图片存储目录(基础目录下,存储正式/预览图) private File fBackgroundSourceDir; // 图片存储目录(基础目录下,存储正式/预览图)
private File fBackgroundCompressDir; // 新增:压缩图统一存储目录(基础目录下/BackgroundCrops
// JSON配置目录原应用外置存储目录不改变 // JSON配置目录原应用外置存储目录不改变
private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils
private File fModelDir; // 模型文件目录存储JSON配置 private File fModelDir; // 模型文件目录存储JSON配置
@@ -63,9 +66,8 @@ public class BackgroundSourceUtils {
} }
// 上下文用Application Context避免Activity内存泄漏 // 上下文用Application Context避免Activity内存泄漏
this.mContext = context.getApplicationContext(); this.mContext = context.getApplicationContext();
// 初始化目录(分图片目录+JSON目录 // 【核心调整1】实例化初期优先初始化所有必要目录确保实例化完成时目录100%就绪
initPictureDirs(); // 初始化图片操作目录(系统公共目录) initNecessaryDirs();
initJsonDirs(); // 初始化JSON配置目录应用外置存储
// 初始化所有文件(裁剪临时文件/结果文件等) // 初始化所有文件(裁剪临时文件/结果文件等)
initAllFiles(); initAllFiles();
// 加载配置(确保正式/预览Bean是两份独立实例 // 加载配置(确保正式/预览Bean是两份独立实例
@@ -85,26 +87,50 @@ public class BackgroundSourceUtils {
} }
/** /**
* 初始化图片操作目录(核心:系统公共图片目录 /Pictures/PowerBell/ * 【核心新增】统一初始化所有必要目录(实例化初期调用,确保目录优先创建
* 整合图片目录+JSON目录集中管理目录创建逻辑保证实例化完成时所有目录就绪
*/
private void initNecessaryDirs() {
LogUtils.d(TAG, "【实例化初期-目录初始化】开始创建所有必要目录...");
// 1. 初始化图片操作目录(系统公共目录 /Pictures/PowerBell/
initPictureDirs();
// 2. 初始化JSON配置目录应用外置存储
initJsonDirs();
LogUtils.d(TAG, "【实例化初期-目录初始化】所有必要目录创建完成!");
}
/**
* 初始化图片操作目录(核心:系统公共图片目录 /Pictures/PowerBell/,新增压缩图目录)
* 【调整强化】新增目录创建后二次校验,失败则降级到备选目录,确保目录可用
*/ */
private void initPictureDirs() { private void initPictureDirs() {
// 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell // 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell
fPictureBaseDir = new File(PICTURE_BASE_DIR); fPictureBaseDir = new File(PICTURE_BASE_DIR);
// 2. 图片存储目录:基础目录下(存储正式/预览图 // 2. 图片存储目录:基础目录下(存储正式/预览图)
fBackgroundSourceDir = new File(fPictureBaseDir, "BackgroundSource"); fBackgroundSourceDir = new File(fPictureBaseDir, "BackgroundSource");
// 3. 裁剪缓存目录:基础目录下/cache所有裁剪操作在此目录 // 3. 裁剪缓存目录:基础目录下/cache所有裁剪操作在此目录
fPictureCacheDir = new File(fPictureBaseDir, CROP_TEMP_DIR_NAME); fPictureCacheDir = new File(fPictureBaseDir, CROP_TEMP_DIR_NAME);
// 4. 新增:压缩图统一存储目录(基础目录下/BackgroundCrops所有压缩图放这里
fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_BASE_DIR_NAME);
// 4. 递归创建目录复用FileUtils间接创建简化逻辑 // 5. 强制创建所有图片目录(带二次校验+降级兜底
createDirWithPermission(fPictureBaseDir, "图片基础目录(/Pictures/PowerBell"); createDirWithPermission(fPictureBaseDir, "图片基础目录(/Pictures/PowerBell", true);
createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下)"); createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下)", true);
createDirWithPermission(fPictureCacheDir, "裁剪缓存目录(基础目录/cache"); createDirWithPermission(fPictureCacheDir, "裁剪缓存目录(基础目录/cache", true);
createDirWithPermission(fBackgroundCompressDir, "压缩图统一存储目录(基础目录/BackgroundCrops", true);
LogUtils.d(TAG, "【图片目录初始化】完成:基础目录=" + fPictureBaseDir.getAbsolutePath() + ",裁剪缓存目录=" + fPictureCacheDir.getAbsolutePath()); // 6. 目录创建后最终校验(确保所有目录已就绪)
validatePictureDirs();
LogUtils.d(TAG, "【图片目录初始化】完成:" +
"基础目录=" + fPictureBaseDir.getAbsolutePath() +
",裁剪缓存目录=" + fPictureCacheDir.getAbsolutePath() +
",压缩图目录=" + fBackgroundCompressDir.getAbsolutePath());
} }
/** /**
* 初始化JSON配置目录保留原逻辑应用外置存储 * 初始化JSON配置目录保留原逻辑应用外置存储
* 【调整强化】新增目录创建后二次校验,失败则降级到应用内部缓存目录
*/ */
private void initJsonDirs() { private void initJsonDirs() {
// 1. 工具类根目录(应用外置存储) // 1. 工具类根目录(应用外置存储)
@@ -115,7 +141,8 @@ public class BackgroundSourceUtils {
} }
// 2. 模型文件目录存储JSON配置 // 2. 模型文件目录存储JSON配置
fModelDir = new File(fUtilsDir, "ModelDir"); fModelDir = new File(fUtilsDir, "ModelDir");
createDirWithPermission(fModelDir, "JSON配置目录(应用外置存储)"); // 强制创建JSON目录带二次校验+降级兜底)
createDirWithPermission(fModelDir, "JSON配置目录应用外置存储", true);
// 3. 初始化JSON文件对象两份独立文件对应两份Bean实例 // 3. 初始化JSON文件对象两份独立文件对应两份Bean实例
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json"); currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
@@ -124,6 +151,149 @@ public class BackgroundSourceUtils {
LogUtils.d(TAG, "【JSON目录初始化】完成目录=" + fModelDir.getAbsolutePath() + "正式JSON=" + currentBackgroundBeanFile.getName() + "预览JSON=" + previewBackgroundBeanFile.getName()); LogUtils.d(TAG, "【JSON目录初始化】完成目录=" + fModelDir.getAbsolutePath() + "正式JSON=" + currentBackgroundBeanFile.getName() + "预览JSON=" + previewBackgroundBeanFile.getName());
} }
/**
* 【核心强化】创建目录并设置权限(适配系统公共目录/Pictures/PowerBell确保实例化时目录就绪
* @param dir 要创建的目录
* @param dirDesc 目录描述(用于日志打印)
* @param needFallback 是否需要降级兜底实例化初期必须为true确保目录可用
*/
private void createDirWithPermission(File dir, String dirDesc, boolean needFallback) {
if (dir == null) {
LogUtils.e(TAG, "【文件管理】创建目录失败目录对象为null描述" + dirDesc + "");
return;
}
// 第一步:主动检测并创建目录(递归创建所有父目录)
boolean isCreated = true;
if (!dir.exists()) {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "不存在,开始创建:" + dir.getAbsolutePath());
isCreated = dir.mkdirs(); // 递归创建所有父目录
} else {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "已存在:" + dir.getAbsolutePath());
}
// 第二步:创建后二次校验(确保目录真的存在且是目录)
if (!dir.exists() || !dir.isDirectory()) {
LogUtils.w(TAG, "【文件管理】" + dirDesc + "创建/校验失败,路径:" + dir.getAbsolutePath());
// 第三步:需要降级时,自动切换到应用内部缓存目录(兜底保障)
if (needFallback) {
File fallbackDir = getFallbackDir(dirDesc);
if (fallbackDir != null) {
LogUtils.w(TAG, "【文件管理】" + dirDesc + "降级到备选目录:" + fallbackDir.getAbsolutePath());
// 更新目录引用为备选目录(确保后续业务调用使用有效目录)
updateDirReference(dir, fallbackDir);
// 强制创建备选目录
if (!fallbackDir.exists()) {
fallbackDir.mkdirs();
}
// 二次校验备选目录
if (fallbackDir.exists() && fallbackDir.isDirectory()) {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "备选目录创建成功:" + fallbackDir.getAbsolutePath());
} else {
LogUtils.e(TAG, "【文件管理】" + dirDesc + "备选目录创建失败,功能可能异常!");
}
}
}
return;
}
// 第四步:强制设置目录权限(递归设置,确保系统公共目录所有层级可读写)
setDirPermissionsRecursively(dir);
// 第五步校验目录实际可写性解决Android14+ canWrite()假阳性问题)
if (!isDirActuallyWritable(dir)) {
LogUtils.w(TAG, "【文件管理】" + dirDesc + "存在但不可写,尝试重新设置权限");
setDirPermissionsRecursively(dir);
// 再次校验仍失败,降级兜底
if (needFallback && !isDirActuallyWritable(dir)) {
File fallbackDir = getFallbackDir(dirDesc);
if (fallbackDir != null) {
LogUtils.w(TAG, "【文件管理】" + dirDesc + "不可写,降级到备选目录:" + fallbackDir.getAbsolutePath());
updateDirReference(dir, fallbackDir);
fallbackDir.mkdirs();
setDirPermissionsRecursively(fallbackDir);
}
}
}
LogUtils.d(TAG, "【文件管理】" + dirDesc + "创建+权限设置完成:路径=" + dir.getAbsolutePath() + ",可写=" + dir.canWrite());
}
/**
* 【新增】获取目录降级备选目录(实例化初期目录创建失败时兜底)
* 优先使用应用内部缓存目录(无需额外权限,兼容性最好)
*/
private File getFallbackDir(String dirDesc) {
File cacheDir = mContext.getCacheDir();
if (cacheDir == null) {
LogUtils.e(TAG, "【文件管理】应用内部缓存目录不可用,无法降级:" + dirDesc);
return null;
}
// 根据目录类型创建对应备选目录(保持目录结构一致)
if (dirDesc.contains("图片基础目录")) {
return new File(cacheDir, "PowerBell");
} else if (dirDesc.contains("图片存储目录")) {
return new File(cacheDir, "PowerBell/BackgroundSource");
} else if (dirDesc.contains("裁剪缓存目录")) {
return new File(cacheDir, "PowerBell/cache");
} else if (dirDesc.contains("压缩图统一存储目录")) {
return new File(cacheDir, "PowerBell/BackgroundCrops");
} else if (dirDesc.contains("JSON配置目录")) {
return new File(cacheDir, "BackgroundSourceUtils/ModelDir");
} else {
return new File(cacheDir, "FallbackDir");
}
}
/**
* 【新增】更新目录引用(目录创建失败降级时,同步更新全局目录变量)
*/
private void updateDirReference(File oldDir, File newDir) {
if (oldDir == null || newDir == null) {
return;
}
// 匹配全局目录变量,更新为备选目录
if (oldDir.equals(fPictureBaseDir)) {
fPictureBaseDir = newDir;
} else if (oldDir.equals(fBackgroundSourceDir)) {
fBackgroundSourceDir = newDir;
} else if (oldDir.equals(fPictureCacheDir)) {
fPictureCacheDir = newDir;
} else if (oldDir.equals(fBackgroundCompressDir)) {
fBackgroundCompressDir = newDir;
} else if (oldDir.equals(fModelDir)) {
fModelDir = newDir;
}
}
/**
* 【新增】图片目录创建后最终校验(确保实例化时所有图片目录已就绪)
*/
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 (!fPictureCacheDir.exists() || !fPictureCacheDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】裁剪缓存目录未就绪:" + fPictureCacheDir.getAbsolutePath());
allReady = false;
}
if (!fBackgroundCompressDir.exists() || !fBackgroundCompressDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】压缩图目录未就绪:" + fBackgroundCompressDir.getAbsolutePath());
allReady = false;
}
if (allReady) {
LogUtils.d(TAG, "【图片目录校验】所有图片目录均已就绪!");
} else {
LogUtils.e(TAG, "【图片目录校验】部分目录未就绪,可能影响后续功能!");
}
}
/** /**
* 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录) * 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录)
*/ */
@@ -136,71 +306,149 @@ public class BackgroundSourceUtils {
// 3. 初始化时清理旧文件复用FileUtils简化清理逻辑 // 3. 初始化时清理旧文件复用FileUtils简化清理逻辑
clearOldFile(cropTempFile, "旧裁剪临时文件(/Pictures/PowerBell/cache"); clearOldFile(cropTempFile, "旧裁剪临时文件(/Pictures/PowerBell/cache");
clearOldFile(cropResultFile, "旧裁剪结果文件(/Pictures/PowerBell/BackgroundSource"); clearOldFile(cropResultFile, "旧裁剪结果文件(/Pictures/PowerBell/BackgroundSource");
// 新增:清理压缩图目录下的旧文件(避免残留)
clearOldCompressFiles();
LogUtils.d(TAG, "【文件初始化】完成:裁剪临时文件=" + cropTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath()); LogUtils.d(TAG, "【文件初始化】完成:裁剪临时文件=" + cropTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath());
} }
/** /**
* 核心函数:为系统裁剪应用创建可读写的FileProvider路径适配Android14+ MIUI * 核心优化函数:带原图参数的裁剪路径创建(优先使用原图,不复制
* 裁剪文件统一放入 /Pictures/PowerBell/cache/,确保系统裁剪工具可读写 * 替代原逻辑中"复制原图到BackgroundSource再裁剪"的流程
*/ */
public File createCropFileProviderPath() { public File createCropFileProviderPath(File originalImageFile) {
LogUtils.d(TAG, "【裁剪路径】createCropFileProviderPath 触发,创建系统裁剪可读写路径"); final String TAG = "BackgroundSourceUtils";
Log.d(TAG, "【裁剪优化】createCropFileProviderPath(原图) 触发,优先使用原图路径");
// 优先使用图片基础目录下的cache目录核心系统公共目录权限更友好 // 核心逻辑1直接使用裁剪前的原图不复制仅校验合法性和权限
if (fPictureCacheDir != null && fPictureCacheDir.exists() && isDirActuallyWritable(fPictureCacheDir)) { if (originalImageFile != null && originalImageFile.exists()
try { && originalImageFile.isFile() && originalImageFile.length() > 0) {
// 先清理旧文件,避免锁定 // 校验原图目录是否可写(系统裁剪工具需读写权限)
clearOldFile(cropTempFile, "裁剪临时文件(重新初始化)"); if (isDirectoryWritable(originalImageFile.getParentFile())) {
// 创建新的裁剪临时文件 // 强制开放原图权限(解决跨进程访问问题)
cropTempFile.createNewFile(); setFileReadWritePermission(originalImageFile);
// 强制设置权限(系统裁剪应用必需:允许所有用户读写) Log.d(TAG, "【裁剪优化】直接使用原图启动裁剪(无复制):" + originalImageFile.getAbsolutePath());
setFilePermissions(cropTempFile); return originalImageFile; // 直接返回原图,不做任何复制
LogUtils.d(TAG, "【裁剪路径】创建成功(/Pictures/PowerBell/cache" + cropTempFile.getAbsolutePath()); } else {
return cropTempFile; Log.w(TAG, "【裁剪优化】原图目录不可写,切换到临时文件兜底");
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】cache目录创建文件失败" + e.getMessage(), e);
} }
} else { } else {
LogUtils.w(TAG, "【裁剪路径】cache目录不可用切换到图片存储目录兜底"); Log.w(TAG, "【裁剪优化】原图无效(空/不存在/0大小切换到临时文件兜底");
} }
// 兜底:使用图片存储目录(极端情况 // 兜底逻辑仅原图不可用时触发避免多复制使用应用缓存目录不涉及BackgroundSource
if (isDirActuallyWritable(fBackgroundSourceDir)) { File cacheDir = mContext.getCacheDir(); // 应用内部缓存,无需额外权限
if (isDirectoryWritable(cacheDir)) {
try { try {
cropTempFile = new File(fBackgroundSourceDir, CROP_TEMP_FILE_NAME); File cropTempFile = new File(cacheDir, "crop_temp_" + System.currentTimeMillis() + ".jpg");
clearOldFile(cropTempFile, "裁剪临时文件(兜底目录)"); // 清理旧临时文件(避免堆积)
if (cropTempFile.exists()) {
cropTempFile.delete();
}
cropTempFile.createNewFile(); cropTempFile.createNewFile();
setFilePermissions(cropTempFile); setFileReadWritePermission(cropTempFile);
LogUtils.w(TAG, "【裁剪路径】切换到图片存储目录创建成功" + cropTempFile.getAbsolutePath()); Log.d(TAG, "【裁剪优化】原图不可用,使用缓存临时文件" + cropTempFile.getAbsolutePath());
return cropTempFile; return cropTempFile;
} catch (IOException e) { } catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】图片存储目录创建文件失败:" + e.getMessage(), e); Log.e(TAG, "【裁剪优化】创建缓存临时文件失败:" + e.getMessage(), e);
} }
} }
// 终极兜底:应用内部缓存目录(提示用户权限问题 // 终极兜底:公共图片缓存目录(极端情况
File cacheDir = mContext.getCacheDir(); File publicCacheDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Cache");
if (isDirActuallyWritable(cacheDir)) { if (isDirectoryWritable(publicCacheDir)) {
try { try {
cropTempFile = new File(cacheDir, CROP_TEMP_FILE_NAME); if (!publicCacheDir.exists()) {
clearOldFile(cropTempFile, "裁剪临时文件(终极兜底)"); publicCacheDir.mkdirs();
}
File cropTempFile = new File(publicCacheDir, "crop_temp_" + System.currentTimeMillis() + ".jpg");
if (cropTempFile.exists()) {
cropTempFile.delete();
}
cropTempFile.createNewFile(); cropTempFile.createNewFile();
setFilePermissions(cropTempFile); setFileReadWritePermission(cropTempFile);
LogUtils.w(TAG, "【裁剪路径】系统公共目录失败兜底到应用缓存MIUI可能裁剪失败" + cropTempFile.getAbsolutePath()); Log.w(TAG, "【裁剪优化】应用缓存不可用,使用公共图片缓存" + cropTempFile.getAbsolutePath());
ToastUtils.show("存储权限受限,建议授予「所有文件访问权限」以确保裁剪正常");
return cropTempFile; return cropTempFile;
} catch (IOException e) { } catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】终极兜底目录创建失败:" + e.getMessage(), e); Log.e(TAG, "【裁剪优化】创建公共临时文件失败:" + e.getMessage(), e);
ToastUtils.show("裁剪路径创建失败,请重启应用并授予存储权限");
} }
} }
// 极端情况:所有目录均失败 Log.e(TAG, "【裁剪优化】所有裁剪路径创建失败,裁剪功能不可用");
LogUtils.e(TAG, "【裁剪路径】所有目录均无法创建裁剪文件,裁剪功能不可用");
return null; return null;
} }
/**
* 【新增辅助方法】创建裁剪结果临时文件(仅用于接收裁剪输出,不涉及原图复制)
* 避免使用工具类的createCropFileProviderPath原方法可能存在复制逻辑
*/
public File createCropResultTempFile() {
// 优先使用应用缓存目录(无需额外权限,避免存储权限问题)
File cacheDir = mContext.getCacheDir();
if (isDirectoryWritable(cacheDir)) {
try {
String tempFileName = "crop_result_" + System.currentTimeMillis() + ".jpg";
File cropTempFile = new File(cacheDir, tempFileName);
// 清理旧临时文件(避免堆积)
if (cropTempFile.exists()) {
cropTempFile.delete();
}
cropTempFile.createNewFile();
// 开放读写权限(供系统裁剪工具访问)
setFileReadWritePermission(cropTempFile);
LogUtils.d(TAG, "【裁剪优化】裁剪结果临时文件创建成功:" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪优化】创建缓存临时文件失败:" + e.getMessage(), e);
}
}
// 兜底:应用外部临时目录
File externalTempDir = new File(App.getTempDirPath());
if (isDirectoryWritable(externalTempDir)) {
try {
String tempFileName = "crop_result_" + System.currentTimeMillis() + ".jpg";
File cropTempFile = new File(externalTempDir, tempFileName);
if (cropTempFile.exists()) {
cropTempFile.delete();
}
cropTempFile.createNewFile();
setFileReadWritePermission(cropTempFile);
LogUtils.w(TAG, "【裁剪优化】应用缓存不可用,使用外部临时目录:" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪优化】创建外部临时文件失败:" + e.getMessage(), e);
}
}
LogUtils.e(TAG, "【裁剪优化】裁剪结果临时文件创建失败");
return null;
}
/**
* 兼容旧调用的无参方法(避免影响其他代码)
*/
public File createCropFileProviderPath() {
return createCropFileProviderPath(null); // 无原图时走兜底逻辑
}
// 辅助方法:校验目录是否可写(新增,保障裁剪权限)
private boolean isDirectoryWritable(File dir) {
if (dir == null) return false;
// 目录存在且可写,或目录不存在但能创建
return (dir.exists() && dir.isDirectory() && dir.canWrite())
|| (!dir.exists() && dir.mkdirs());
}
// 辅助方法:设置文件读写权限(新增,解决系统裁剪跨进程访问)
private void setFileReadWritePermission(File file) {
if (file == null || !file.exists()) return;
// 开放读写权限兼容Android 10+
file.setReadable(true, false);
file.setWritable(true, false);
file.setExecutable(true, false);
}
/** /**
* 加载背景图片配置数据核心确保current/preview是两份独立的BackgroundBean实例 * 加载背景图片配置数据核心确保current/preview是两份独立的BackgroundBean实例
*/ */
@@ -211,6 +459,9 @@ public class BackgroundSourceUtils {
currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化 currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
LogUtils.d(TAG, "【配置管理】正式背景Bean不存在创建独立实例并保存到JSON"); LogUtils.d(TAG, "【配置管理】正式背景Bean不存在创建独立实例并保存到JSON");
} else {
// 修复加载旧配置时若压缩图路径不在BackgroundCrops自动迁移路径兼容历史数据
migrateCompressPathToNewDir(currentBackgroundBean, true);
} }
// 2. 加载预览Bean独立实例从previewBackgroundBean.json加载不存在则新建与正式Bean完全分离 // 2. 加载预览Bean独立实例从previewBackgroundBean.json加载不存在则新建与正式Bean完全分离
@@ -219,6 +470,9 @@ public class BackgroundSourceUtils {
previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化 previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "【配置管理】预览背景Bean不存在创建独立实例并保存到JSON"); LogUtils.d(TAG, "【配置管理】预览背景Bean不存在创建独立实例并保存到JSON");
} else {
// 修复加载旧配置时若压缩图路径不在BackgroundCrops自动迁移路径兼容历史数据
migrateCompressPathToNewDir(previewBackgroundBean, false);
} }
LogUtils.d(TAG, "【配置管理】两份Bean实例初始化完成正式Bean=" + currentBackgroundBean.hashCode() + "预览Bean=" + previewBackgroundBean.hashCode() + "hash不同证明实例独立"); LogUtils.d(TAG, "【配置管理】两份Bean实例初始化完成正式Bean=" + currentBackgroundBean.hashCode() + "预览Bean=" + previewBackgroundBean.hashCode() + "hash不同证明实例独立");
} }
@@ -261,7 +515,7 @@ public class BackgroundSourceUtils {
} }
/** /**
* 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验) * 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验统一指向BackgroundCrops目录
*/ */
public String getPreviewBackgroundScaledCompressFilePath() { public String getPreviewBackgroundScaledCompressFilePath() {
String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
@@ -269,8 +523,24 @@ public class BackgroundSourceUtils {
LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径"); LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径");
return ""; return "";
} }
File file = new File(fBackgroundSourceDir, compressFileName); // 关键压缩图路径统一指向BackgroundCrops目录不再用BackgroundSource
LogUtils.d(TAG, "【路径管理】预览压缩背景路径:" + file.getAbsolutePath()); 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, "【路径管理】正式压缩背景文件名为空,返回空路径");
return "";
}
// 关键压缩图路径统一指向BackgroundCrops目录
File file = new File(fBackgroundCompressDir, compressFileName);
LogUtils.d(TAG, "【路径管理】正式压缩背景路径BackgroundCrops目录" + file.getAbsolutePath());
return file.getAbsolutePath(); return file.getAbsolutePath();
} }
@@ -290,6 +560,13 @@ public class BackgroundSourceUtils {
return fBackgroundSourceDir.getAbsolutePath(); return fBackgroundSourceDir.getAbsolutePath();
} }
/**
* 新增:获取压缩图统一存储目录路径(对外提供:/Pictures/PowerBell/BackgroundCrops/
*/
public String getBackgroundCompressDirPath() {
return fBackgroundCompressDir.getAbsolutePath();
}
public File getCropTempFile() { public File getCropTempFile() {
return cropTempFile; return cropTempFile;
} }
@@ -304,130 +581,101 @@ public class BackgroundSourceUtils {
// ------------------------------ 核心业务方法复用FileUtils简化文件操作------------------------------ // ------------------------------ 核心业务方法复用FileUtils简化文件操作------------------------------
/** /**
* 保存图片到预览Bean图片存储到/Pictures/PowerBell/BackgroundSourceJSON仍存应用外置存储 * 优化函数:仅裁剪结果图可保存到BackgroundSource(避免启动裁剪时误复制原图
* @param sourceFile 源图片文件(非空,必须存在) * 说明:启动裁剪时不调用此方法,仅在裁剪完成后保存结果图时调用
* @param fileInfo 图片附加信息如Uri字符串仅作备注
* @return 更新后的预览Bean
*/ */
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) { public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
// 强化校验:源文件必须存在、是文件、大小>0避免无效文件复制 final String TAG = "BackgroundSourceUtils";
if (sourceFile == null || !sourceFile.exists() || !sourceFile.isFile() || sourceFile.length() <= 0) { // 强化校验1仅允许裁剪结果图传入通过文件路径判断避免原图误传入
LogUtils.e(TAG, "【文件管理】源文件无效:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null") + ",大小:" + (sourceFile != null ? sourceFile.length() : 0) + "bytes"); if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) {
ToastUtils.show("源图片文件无效"); Log.e(TAG, "【保存优化】源文件无效,拒绝保存:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
return previewBackgroundBean; return previewBackgroundBean;
} }
// 确保图片存储目录存在(/Pictures/PowerBell/BackgroundSource // 强化校验2排除原图路径避免启动裁剪时传入原图复制
String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); // 原图存储目录(如相册/拍照目录)
if (sourceFile.getAbsolutePath().contains(originalImageDir)) {
Log.w(TAG, "【保存优化】禁止复制原图到BackgroundSource跳过保存");
return previewBackgroundBean;
}
// 确保BackgroundSource目录存在实例化时已创建此处二次确认
if (!fBackgroundSourceDir.exists()) { if (!fBackgroundSourceDir.exists()) {
createDirWithPermission(fBackgroundSourceDir, "图片存储目录saveFileToPreviewBean"); if (!fBackgroundSourceDir.mkdirs()) {
} Log.e(TAG, "【保存优化】BackgroundSource目录创建失败");
// 生成唯一文件名直接复用FileUtils工具方法无需重复实现
String uniqueFileName = FileUtils.createUniqueFileName(sourceFile);
File previewBackgroundFile = new File(fBackgroundSourceDir, uniqueFileName);
// 核心用FileUtils.copyFile替代自定义流复制精简代码且保证高效
boolean copySuccess = FileUtils.copyFile(sourceFile, previewBackgroundFile);
if (!copySuccess) {
LogUtils.e(TAG, "【文件管理】图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + "" + previewBackgroundFile.getAbsolutePath());
ToastUtils.show("预览图片保存失败");
return previewBackgroundBean; return previewBackgroundBean;
} }
}
// 预览Bean赋值仍用独立实例不影响正式Bean // 生成唯一文件名(避免覆盖
previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 唯一文件名(非空) String uniqueFileName = "bg_" + System.currentTimeMillis() + "_" + sourceFile.getName();
previewBackgroundBean.setBackgroundFilePath(previewBackgroundFile.getAbsolutePath()); // 新增:设置预览原图完整路径 File targetFile = new File(fBackgroundSourceDir, uniqueFileName);
previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_" + previewBackgroundFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(fBackgroundSourceDir.getAbsolutePath() + File.separator + "ScaledCompress_" + previewBackgroundFile.getName()); // 新增:设置预览压缩图完整路径 // 执行复制(仅裁剪结果图会走到这一步)
if (FileUtils.copyFile(sourceFile, targetFile)) {
Log.d(TAG, "【保存优化】裁剪结果图保存成功:" + targetFile.getAbsolutePath());
// 更新预览Bean原有逻辑保留
previewBackgroundBean.setBackgroundFileName(uniqueFileName);
previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath());
previewBackgroundBean.setBackgroundFileInfo(fileInfo); previewBackgroundBean.setBackgroundFileInfo(fileInfo);
previewBackgroundBean.setIsUseBackgroundFile(true); previewBackgroundBean.setIsUseBackgroundFile(true);
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(true); // 重命名字段:设置启用压缩图 // 保存Bean到本地原有逻辑保留
previewBackgroundBean.setBackgroundWidth(100); saveSettings();
previewBackgroundBean.setBackgroundHeight(100); } else {
Log.e(TAG, "【保存优化】裁剪结果图复制失败:" + sourceFile.getAbsolutePath() + "" + targetFile.getAbsolutePath());
}
// 强制保存预览Bean到对应JSON不影响正式Bean的JSON
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "【文件管理】预览Bean强制保存到JSON" + previewBackgroundBeanFile.getAbsolutePath());
LogUtils.d(TAG, "【文件管理】预览图片保存成功(/Pictures/PowerBell" + previewBackgroundFile.getAbsolutePath() + ",大小:" + previewBackgroundFile.length() + "bytes");
ToastUtils.show("预览图片加载成功");
return previewBackgroundBean; return previewBackgroundBean;
} }
/** /**
* 提交预览背景到正式背景预览Bean → 正式Bean深拷贝新建正式Bean实例+逐字段拷贝) * 提交预览背景到正式背景预览Bean → 正式Bean深拷贝新建正式Bean实例+逐字段拷贝)
* 核心深拷贝后修改正式Bean不会影响预览Bean两份实例完全独立 * 核心深拷贝后修改正式Bean不会影响预览Bean两份实例完全独立压缩图路径统一指向BackgroundCrops
*/ */
public void commitPreviewSourceToCurrent() { public void commitPreviewSourceToCurrent() {
// 深拷贝第一步新建正式Bean独立实例彻底脱离预览Bean的引用 // 深拷贝第一步新建正式Bean独立实例彻底脱离预览Bean的引用
currentBackgroundBean = new BackgroundBean(); currentBackgroundBean = new BackgroundBean();
// 深拷贝第二步逐字段拷贝预览Bean的所有值确保字段无遗漏 // 深拷贝第二步逐字段拷贝预览Bean的所有值压缩图路径同步指向BackgroundCrops
currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName()); currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName());
currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); // 新增字段:拷贝原图完整路径 currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); // 原图路径BackgroundSource
currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo()); currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo());
currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile()); currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile());
currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName()); currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName());
currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); // 新增字段:拷贝压缩图完整路径 currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径BackgroundCrops
currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态
currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth()); currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth());
currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight()); currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight());
currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor()); currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor());
saveSettings(); // 分别保存正式Bean→currentJSON预览Bean→previewJSON两份独立 saveSettings(); // 分别保存正式Bean→currentJSON预览Bean→previewJSON两份独立
LogUtils.d(TAG, "【配置管理】预览背景深拷贝到正式Bean两份实例独立JSON分别存储"); LogUtils.d(TAG, "【配置管理】预览背景深拷贝到正式Bean两份实例独立压缩图统一存储到BackgroundCrops");
ToastUtils.show("背景图片应用成功"); ToastUtils.show("背景图片应用成功");
} }
/** /**
* 将正式背景同步到预览背景正式Bean → 预览Bean深拷贝新建预览Bean实例+逐字段拷贝) * 将正式背景同步到预览背景正式Bean → 预览Bean深拷贝新建预览Bean实例+逐字段拷贝)
* 核心深拷贝后修改预览Bean不会影响正式Bean两份实例完全独立 * 核心深拷贝后修改预览Bean不会影响正式Bean两份实例完全独立压缩图路径统一指向BackgroundCrops
*/ */
public void setCurrentSourceToPreview() { public void setCurrentSourceToPreview() {
// 深拷贝第一步新建预览Bean独立实例彻底脱离正式Bean的引用 // 深拷贝第一步新建预览Bean独立实例彻底脱离正式Bean的引用
previewBackgroundBean = new BackgroundBean(); previewBackgroundBean = new BackgroundBean();
// 深拷贝第二步逐字段拷贝正式Bean的所有值确保字段无遗漏 // 深拷贝第二步逐字段拷贝正式Bean的所有值压缩图路径同步指向BackgroundCrops
previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName()); previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName());
previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); // 新增字段:拷贝原图完整路径 previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); // 原图路径BackgroundSource
previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo()); previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo());
previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile()); previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile());
previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName()); previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); // 新增字段:拷贝压缩图完整路径 previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径BackgroundCrops
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态
previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth()); previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth());
previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight()); previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight());
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
saveSettings(); // 分别保存正式Bean→currentJSON预览Bean→previewJSON两份独立 saveSettings(); // 分别保存正式Bean→currentJSON预览Bean→previewJSON两份独立
LogUtils.d(TAG, "【配置管理】正式背景深拷贝到预览Bean两份实例独立JSON分别存储"); LogUtils.d(TAG, "【配置管理】正式背景深拷贝到预览Bean两份实例独立压缩图统一存储到BackgroundCrops");
} }
// ------------------------------ 必需保留的工具方法(与业务/权限强相关无法复用FileUtils------------------------------ // ------------------------------ 必需保留的工具方法(与业务/权限强相关无法复用FileUtils------------------------------
/**
* 工具方法:创建目录并设置权限(适配系统公共目录/Pictures/PowerBell确保可读写
* @param dir 要创建的目录
* @param dirDesc 目录描述(用于日志打印)
*/
private void createDirWithPermission(File dir, String dirDesc) {
if (dir == null) {
LogUtils.e(TAG, "【文件管理】创建目录失败目录对象为null描述" + dirDesc + "");
return;
}
if (!dir.exists()) {
boolean mkdirsSuccess = dir.mkdirs();
LogUtils.d(TAG, "【文件管理】" + dirDesc + "创建结果:" + (mkdirsSuccess ? "成功" : "失败") + ",路径:" + dir.getAbsolutePath());
if (!mkdirsSuccess) {
LogUtils.w(TAG, "【文件管理】" + dirDesc + "创建失败,尝试创建父目录");
File parentDir = dir.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
dir.mkdir();
}
}
}
// 强制设置目录权限(递归设置,确保系统公共目录所有层级可读写)
setDirPermissionsRecursively(dir);
}
/** /**
* 工具方法:递归设置目录及子目录/文件的读写权限(适配系统公共目录/Pictures/PowerBell * 工具方法:递归设置目录及子目录/文件的读写权限(适配系统公共目录/Pictures/PowerBell
* @param dir 要设置权限的目录 * @param dir 要设置权限的目录
@@ -458,8 +706,10 @@ public class BackgroundSourceUtils {
file.setReadable(true, false); file.setReadable(true, false);
file.setWritable(true, false); file.setWritable(true, false);
file.setExecutable(false, false); file.setExecutable(false, false);
// 裁剪/预览相关文件单独打印日志 // 裁剪/压缩/预览相关文件单独打印日志
if (file.getName().contains(CROP_TEMP_FILE_NAME) || file.getName().contains(CROP_RESULT_FILE_NAME) || file.getName().startsWith("ScaledCompress_")) { if (file.getName().contains(CROP_TEMP_FILE_NAME) ||
file.getName().contains(CROP_RESULT_FILE_NAME) ||
file.getName().startsWith("ScaledCompress_")) {
LogUtils.d(TAG, "【权限管理】关键文件权限设置:文件名=" + file.getName() + ",可写=" + file.canWrite()); LogUtils.d(TAG, "【权限管理】关键文件权限设置:文件名=" + file.getName() + ",可写=" + file.canWrite());
} }
} }
@@ -516,6 +766,24 @@ public class BackgroundSourceUtils {
} }
} }
/**
* 新增:清理压缩图目录下的旧文件(避免残留,初始化时调用)
*/
private void clearOldCompressFiles() {
if (fBackgroundCompressDir.exists()) {
File[] files = fBackgroundCompressDir.listFiles();
if (files != null && files.length > 0) {
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isFile() && file.getName().startsWith("ScaledCompress_")) {
clearOldFile(file, "压缩图目录旧文件BackgroundCrops");
}
}
}
}
LogUtils.d(TAG, "【文件管理】压缩图目录旧文件清理完成BackgroundCrops");
}
/** /**
* 工具方法验证目录实际写入能力解决Android14+ canWrite()假阳性问题,适配/Pictures/PowerBell * 工具方法验证目录实际写入能力解决Android14+ canWrite()假阳性问题,适配/Pictures/PowerBell
* 原理:通过创建临时空文件并删除,验证目录是否真的可写 * 原理:通过创建临时空文件并删除,验证目录是否真的可写
@@ -594,9 +862,9 @@ public class BackgroundSourceUtils {
LogUtils.e(TAG, "【文件管理】目录创建失败目标目录对象为null"); LogUtils.e(TAG, "【文件管理】目录创建失败目标目录对象为null");
return false; return false;
} }
// 若target是文件取其父目录若本身是目录直接创建适配/Pictures/PowerBell目录 // 若target是文件取其父目录若本身是目录直接创建实例化时已创建,此处二次确认
File targetDir = target.isFile() ? target.getParentFile() : target; File targetDir = target.isFile() ? target.getParentFile() : target;
createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下"); createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下", false);
LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath()); LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath());
return true; return true;
} }
@@ -619,8 +887,12 @@ public class BackgroundSourceUtils {
String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : ""; String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : "";
String cachePath = mContext.getCacheDir().getAbsolutePath(); String cachePath = mContext.getCacheDir().getAbsolutePath();
if (!TextUtils.isEmpty(publicPicturePath) && dirPath.contains(publicPicturePath + File.separator + "PowerBell")) { if (!TextUtils.isEmpty(publicPicturePath)) {
if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_BASE_DIR_NAME)) {
return "系统公共图片目录(/Pictures/PowerBell/BackgroundCrops压缩图统一存储目录"; // 新增压缩图目录描述
} else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) {
return "系统公共图片目录(/Pictures/PowerBell图片存储/裁剪目录)"; return "系统公共图片目录(/Pictures/PowerBell图片存储/裁剪目录)";
}
} else if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) { } else if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) {
return "应用私有外部目录getExternalFilesDir()JSON配置目录"; return "应用私有外部目录getExternalFilesDir()JSON配置目录";
} else if (dirPath.contains(cachePath)) { } else if (dirPath.contains(cachePath)) {
@@ -628,9 +900,58 @@ public class BackgroundSourceUtils {
} else { } else {
return "外部存储目录(非应用私有,权限受限)"; return "外部存储目录(非应用私有,权限受限)";
} }
return "未知目录";
} }
/**
* 新增迁移旧压缩图路径到新目录BackgroundCrops兼容历史数据
* @param bean 要迁移的BackgroundBean正式/预览)
* @param isCurrentBean 是否是正式Bean用于日志区分
*/
private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) {
String oldCompressPath = bean.getBackgroundScaledCompressFilePath();
String beanType = isCurrentBean ? "正式Bean" : "预览Bean";
// 校验旧路径非空且不在BackgroundCrops目录下才需要迁移
if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) {
LogUtils.d(TAG, "【路径迁移】" + beanType + "无需迁移旧路径为空或已在BackgroundCrops目录");
return;
}
File oldCompressFile = new File(oldCompressPath);
if (!oldCompressFile.exists() || !oldCompressFile.isFile() || oldCompressFile.length() <= 0) {
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());
}
return;
}
// 迁移逻辑复制旧文件到新目录更新Bean路径删除旧文件
String compressFileName = bean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
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());
} else {
LogUtils.e(TAG, "【路径迁移】" + beanType + "压缩文件复制失败,迁移终止:" + oldCompressPath);
}
}
// ======================================== 核心实现:获取图片旋转角度 ======================================== // ======================================== 核心实现:获取图片旋转角度 ========================================
/** /**
@@ -692,19 +1013,19 @@ public class BackgroundSourceUtils {
// ======================================== 图片处理核心方法(压缩/裁剪/保存) ======================================== // ======================================== 图片处理核心方法(压缩/裁剪/保存) ========================================
/** /**
* 压缩图片并保存(核心修复:路径非空校验+兜底路径,Java7 手动管理流 * 压缩图片并保存(核心修复:路径非空校验+兜底路径,统一存储到BackgroundCrops目录
*/ */
public void compressQualityToRecivedPicture(Bitmap bitmap) { public void compressQualityToRecivedPicture(Bitmap bitmap) {
// 兼容裁剪等旧调用:从工具类获取默认压缩路径,转发至重载函数 // 兼容裁剪等旧调用:从工具类获取默认压缩路径统一指向BackgroundCrops,转发至重载函数
String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath(); String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath();
compressQualityToRecivedPicture(bitmap, defaultCompressPath); compressQualityToRecivedPicture(bitmap, defaultCompressPath);
} }
/** /**
* 重载方法指定路径压缩图片并保存修复压缩后同步路径到预览Bean * 重载方法指定路径压缩图片并保存修复压缩后同步路径到预览Bean统一存储到BackgroundCrops
* 适配场景裁剪后生成压缩图强制绑定路径到预览Bean避免路径错位 * 适配场景裁剪后生成压缩图强制绑定路径到预览Bean避免路径错位
* @param bitmap 待压缩的Bitmap裁剪后的缩放图 * @param bitmap 待压缩的Bitmap裁剪后的缩放图
* @param targetCompressPath 强制指定的压缩目标路径从预览Bean获取/生成) * @param targetCompressPath 强制指定的压缩目标路径从预览Bean获取/生成默认指向BackgroundCrops
*/ */
public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) { public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) {
LogUtils.d(TAG, "【压缩启动】开始压缩图片指定路径Bitmap状态" + (bitmap != null && !bitmap.isRecycled())); LogUtils.d(TAG, "【压缩启动】开始压缩图片指定路径Bitmap状态" + (bitmap != null && !bitmap.isRecycled()));
@@ -718,22 +1039,27 @@ public class BackgroundSourceUtils {
FileOutputStream fos = null; FileOutputStream fos = null;
try { try {
String scaledCompressFilePath = targetCompressPath; String scaledCompressFilePath = targetCompressPath;
// 兜底:若传入路径为空,生成临时压缩路径(兼容异常场景) // 兜底:若传入路径为空,生成BackgroundCrops目录下的临时压缩路径(兼容异常场景)
if (TextUtils.isEmpty(scaledCompressFilePath)) { if (TextUtils.isEmpty(scaledCompressFilePath)) {
LogUtils.e(TAG, "【压缩异常】指定路径为空,使用临时兜底路径"); LogUtils.e(TAG, "【压缩异常】指定路径为空,使用BackgroundCrops临时兜底路径");
File tempDir = new File(App.getTempDirPath(), "PreviewCompress"); // 多包名环境下临时目录隔离 // 强制在BackgroundCrops目录下生成临时路径不再用其他目录
if (!tempDir.exists()) { String tempCompressName = "preview_compress_" + System.currentTimeMillis() + ".jpg";
tempDir.mkdirs(); scaledCompressFilePath = new File(fBackgroundCompressDir, tempCompressName).getAbsolutePath();
FileUtils.copyFile(new File(""), tempDir); // 复用原有目录创建逻辑 LogUtils.d(TAG, "【压缩兜底】BackgroundCrops临时路径" + scaledCompressFilePath);
} else {
// 强制校验传入路径必须在BackgroundCrops目录下否则重置确保统一存储
if (!scaledCompressFilePath.contains(fBackgroundCompressDir.getAbsolutePath())) {
LogUtils.w(TAG, "【压缩校验】传入路径不在BackgroundCrops目录自动重置");
String compressFileName = new File(scaledCompressFilePath).getName();
scaledCompressFilePath = new File(fBackgroundCompressDir, compressFileName).getAbsolutePath();
LogUtils.d(TAG, "【压缩校验】重置后路径:" + scaledCompressFilePath);
} }
scaledCompressFilePath = new File(tempDir, "preview_compress_" + System.currentTimeMillis() + ".jpg").getAbsolutePath();
LogUtils.d(TAG, "【压缩兜底】临时路径:" + scaledCompressFilePath);
} }
File compressFile = new File(scaledCompressFilePath); File compressFile = new File(scaledCompressFilePath);
LogUtils.d(TAG, "【压缩配置】目标路径:" + scaledCompressFilePath + "Bitmap原始大小" + bitmap.getByteCount() / 1024 + "KB"); LogUtils.d(TAG, "【压缩配置】目标路径BackgroundCrops" + scaledCompressFilePath + "Bitmap原始大小" + bitmap.getByteCount() / 1024 + "KB");
// 确保目录存在(兼容Android 10+分区存储,多包名路径适配 // 确保压缩图目录存在(实例化时已创建,此处二次确认
File parentDir = compressFile.getParentFile(); File parentDir = compressFile.getParentFile();
if (parentDir == null) { if (parentDir == null) {
LogUtils.e(TAG, "【压缩异常】父目录为空,无法创建文件"); LogUtils.e(TAG, "【压缩异常】父目录为空,无法创建文件");
@@ -743,12 +1069,12 @@ public class BackgroundSourceUtils {
if (!parentDir.exists()) { if (!parentDir.exists()) {
parentDir.mkdirs(); parentDir.mkdirs();
FileUtils.copyFile(new File(""), parentDir); FileUtils.copyFile(new File(""), parentDir);
LogUtils.d(TAG, "【压缩准备】目录已创建:" + parentDir.getAbsolutePath()); LogUtils.d(TAG, "【压缩准备】BackgroundCrops目录已创建:" + parentDir.getAbsolutePath());
} }
// 清理旧的压缩文件(避免文件残留) // 清理旧的压缩文件(避免文件残留)
if (compressFile.exists()) { if (compressFile.exists()) {
clearOldFileByExternal(compressFile, "旧压缩文件"); clearOldFileByExternal(compressFile, "旧压缩文件BackgroundCrops");
} }
compressFile.createNewFile(); compressFile.createNewFile();
@@ -768,7 +1094,7 @@ public class BackgroundSourceUtils {
} }
} }
LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + compressFile.length() / 1024 + "KB"); LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + compressFile.length() / 1024 + "KB,路径:" + scaledCompressFilePath);
// 关键修复压缩成功后强制同步路径到预览Bean双重保障避免时序错位 // 关键修复压缩成功后强制同步路径到预览Bean双重保障避免时序错位
if (compressSuccess) { if (compressSuccess) {
@@ -776,7 +1102,7 @@ public class BackgroundSourceUtils {
if (previewBean != null) { if (previewBean != null) {
previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath); previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath);
saveSettings(); // 持久化配置,多包名环境下配置隔离 saveSettings(); // 持久化配置,多包名环境下配置隔离
LogUtils.d(TAG, "【压缩路径同步】已绑定到预览Bean" + scaledCompressFilePath); LogUtils.d(TAG, "【压缩路径同步】已绑定到预览BeanBackgroundCrops" + scaledCompressFilePath);
} else { } else {
LogUtils.e(TAG, "【压缩路径同步失败】预览Bean为空"); LogUtils.e(TAG, "【压缩路径同步失败】预览Bean为空");
} }
@@ -809,4 +1135,3 @@ public class BackgroundSourceUtils {
} }
} }
} }