文件管理模块重构。

This commit is contained in:
2025-12-02 06:01:38 +08:00
parent 33f1b430a4
commit 637d4577df
3 changed files with 276 additions and 559 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Dec 01 18:48:19 GMT 2025
#Mon Dec 01 21:56:35 GMT 2025
stageCount=13
libraryProject=
baseVersion=15.11
publishVersion=15.11.12
buildCount=103
buildCount=106
baseBetaVersion=15.11.13

View File

@@ -75,8 +75,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
private BackgroundView bvPreviewBackground;
// 拍照临时文件(仅拍照用,路径由工具类间接管理)
private File mfTakePhoto;
// 新增:选图临时文件(独立管理,与拍照临时文件隔离)
private File mSelectTempFile;
// 配置标记(是否提交设置)
boolean isCommitSettings = false;
// 预览图片信息(用于退出确认)
@@ -117,8 +115,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
selectTempDir.mkdirs();
LogUtils.d(TAG, "【选图初始化】选图临时目录创建完成:" + selectTempDir.getAbsolutePath());
}
mSelectTempFile = new File(selectTempDir, "selected_temp_" + System.currentTimeMillis() + ".jpg");
// 初始化UI及数据
initToolbar();
initClickListeners();
@@ -240,12 +237,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】触发选择图片功能");
// 新增:选图前清理旧的选图临时文件(避免残留文件干扰)
if (mSelectTempFile != null && mSelectTempFile.exists()) {
boolean deleteSuccess = mSelectTempFile.delete();
LogUtils.d(TAG, "【选图准备】旧选图临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + mSelectTempFile.getAbsolutePath());
}
// 调用权限工具类校验存储权限
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
LogUtils.d(TAG, "【选图权限】存储权限已获取,开始查找图片选择意图");
@@ -285,7 +277,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
Intent chooser = Intent.createChooser(validIntent, "选择图片");
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
LogUtils.d(TAG, "【选图意图】找到有效意图,已启动图片选择(临时文件路径:" + mSelectTempFile.getAbsolutePath() + "");
LogUtils.d(TAG, "【选图意图】找到有效意图,已启动图片选择");
} else {
LogUtils.d(TAG, "【选图意图】未找到有效图片选择应用,提示用户安装");
runOnUiThread(new Runnable() {
@@ -489,24 +481,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
public void startCropImageActivity(boolean isCropFree) {
LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree);
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
previewBean.setIsUseBackgroundScaledCompressFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(false);
mBgSourceUtils.saveSettings();
bvPreviewBackground.reloadPreviewBackground();
// 1. 预览图片有效性校验(直接使用预览图原图,不复制)
String previewFilePath = mBgSourceUtils.getPreviewBackgroundFilePath();
if (TextUtils.isEmpty(previewFilePath)) {
ToastUtils.show("预览图片路径为空");
LogUtils.e(TAG, "【裁剪失败】预览图片路径为空");
return;
}
File previewFile = new File(previewFilePath); // 裁剪原图:直接使用预览图文件
LogUtils.d(TAG, "【裁剪优化】直接使用预览图原图启动裁剪(不复制):" + previewFile.getAbsolutePath());
LogUtils.d(TAG, "【裁剪校验】预览图片状态:是否存在=" + previewFile.exists() + ",是否为文件=" + previewFile.isFile() + ",大小=" + (previewFile.exists() ? previewFile.length() : 0) + "bytes");
if (!previewFile.exists() || !previewFile.isFile() || previewFile.length() <= 100) {
ToastUtils.show("预览图片不存在或损坏");
LogUtils.e(TAG, "【裁剪失败】预览图片无效");
return;
}
File previewFile = mBgSourceUtils.getCropSourceFile(); // 裁剪缓存图片
// 2. 生成裁剪输入Uri强化MIUI权限授予直接用原图Uri
Uri inputUri = null;
@@ -518,13 +497,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
} catch (Exception e) {
LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败" + e.getMessage(), e);
ToastUtils.show("图片裁剪失败:无法获取图片权限");
mBgSourceUtils.clearCropTempFiles();
return;
}
// 3. 【核心优化】直接使用预览图原图作为裁剪输入,删除原工具类创建裁剪路径的复制逻辑
// 仅创建裁剪结果临时文件(用于接收裁剪输出,不涉及原图复制)
File cropResultTempFile = mBgSourceUtils.createCropResultTempFile();
File cropResultTempFile = mBgSourceUtils.getCropResultFile();
if (cropResultTempFile == null) {
ToastUtils.show("裁剪路径创建失败,请重试");
return;
@@ -609,13 +587,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
startActivityForResult(chooser, REQUEST_CROP_IMAGE);
} else {
ToastUtils.show("无可用裁剪工具,请安装系统相机");
mBgSourceUtils.clearCropTempFiles();
}
}
} catch (Exception e) {
LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e);
ToastUtils.show("无法启动裁剪工具");
mBgSourceUtils.clearCropTempFiles();
}
}
@@ -656,7 +632,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
LogUtils.d(TAG, "【保存启动】开始保存裁剪图片仅更新预览Bean不影响正式Bean");
if (bitmap == null || bitmap.isRecycled()) {
ToastUtils.show("裁剪图片为空");
mBgSourceUtils.clearCropTempFiles();
//mBgSourceUtils.clearCropTempFiles();
return;
}
@@ -678,9 +654,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
BufferedOutputStream bos = null;
try {
// 1. 清理旧的裁剪预览图(避免文件残留)
if (cropSaveFile.exists()) {
mBgSourceUtils.clearOldFileByExternal(cropSaveFile, "旧裁剪预览图");
}
// if (cropSaveFile.exists()) {
// mBgSourceUtils.clearOldFileByExternal(cropSaveFile, "旧裁剪预览图");
// }
// 确保父目录存在兼容Android 10+分区存储,多包名环境下目录适配)
File parentDir = cropSaveFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
@@ -737,10 +713,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪保存失败】IO异常" + e.getMessage(), e);
ToastUtils.show("裁剪图片保存失败");
// 异常时清理无效文件
if (cropSaveFile.exists()) {
mBgSourceUtils.clearOldFileByExternal(cropSaveFile, "异常裁剪图");
}
} finally {
// 资源回收避免内存泄漏兼容低版本Android
if (bos != null) {
@@ -766,9 +738,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
}
}
// 5. 清理裁剪临时文件(保留原有逻辑,避免临时文件堆积)
mBgSourceUtils.clearCropTempFiles();
LogUtils.d(TAG, "【裁剪保存】流程结束,临时文件已清理");
LogUtils.d(TAG, "【裁剪保存】流程结束。");
}
/**
@@ -883,7 +853,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
} catch (Exception e) {
LogUtils.e(TAG, "【回调异常】onActivityResult 全局异常:" + e.getMessage(), e);
ToastUtils.show("操作失败,请重试");
mBgSourceUtils.clearCropTempFiles(); // 异常时清理临时文件,避免残留
//mBgSourceUtils.clearCropTempFiles(); // 异常时清理临时文件,避免残留
//clearSelectTempFile(); // 新增:异常时清理选图临时文件
}
}
@@ -913,7 +883,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
// 校验拍照文件有效性
if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) {
ToastUtils.show("拍照文件不存在或损坏");
mBgSourceUtils.clearCropTempFiles();
//mBgSourceUtils.clearCropTempFiles();
return;
}
@@ -923,7 +893,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
mBgSourceUtils.compressQualityToRecivedPicture(photoBitmap);
} else {
ToastUtils.show("拍照图片为空");
mBgSourceUtils.clearCropTempFiles();
//mBgSourceUtils.clearCropTempFiles();
return;
}
@@ -988,7 +958,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
*/
private void handleCropImageResult(int requestCode, int resultCode, Intent data) {
// 从工具类获取裁剪临时文件(唯一入口)
File cropTempFile = mBgSourceUtils.getCropTempFile();
File cropTempFile = mBgSourceUtils.getCropSourceFile();
boolean isFileExist = cropTempFile != null && cropTempFile.exists();
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
long fileSize = isFileExist ? cropTempFile.length() : 0;
@@ -1002,7 +972,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
if (resultCode == 0 && !isCropSuccess) {
LogUtils.d(TAG, "【裁剪回调】MIUI 裁剪工具已取消");
ToastUtils.show("裁剪已取消");
mBgSourceUtils.clearCropTempFiles();
//mBgSourceUtils.clearCropTempFiles();
return;
}
@@ -1010,7 +980,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
if (isFileExist && fileSize == 0) {
LogUtils.e(TAG, "【裁剪失败】裁剪文件为空MIUI适配问题");
ToastUtils.show("裁剪失败,请尝试选择「系统相机」裁剪或更换图片");
mBgSourceUtils.clearCropTempFiles();
//mBgSourceUtils.clearCropTempFiles();
return;
}
@@ -1024,7 +994,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
} else {
ToastUtils.show("获取裁剪图片失败");
LogUtils.e(TAG, "【裁剪回调失败】Bitmap解析异常");
mBgSourceUtils.clearCropTempFiles();
//mBgSourceUtils.clearCropTempFiles();
}
} else {
// 其他失败场景
@@ -1153,8 +1123,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("选择的图片Uri为空");
//clearSelectTempFile(); // 修复:取消时清理选图临时文件
mBgSourceUtils.clearCropTempFiles();
return;
}
LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString());
@@ -1163,36 +1131,23 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
grantPersistableUriPermission(selectedImage);
// 关键修复1解析Uri为文件强制使用独立的选图临时文件避免路径混淆
boolean parseSuccess = parseUriToSelectTempFile(selectedImage);
if (!parseSuccess || mSelectTempFile == null || !mSelectTempFile.exists() || mSelectTempFile.length() <= 0) {
mBgSourceUtils.createCropFileProviderPath(selectedImage);
if (mBgSourceUtils.getCropSourceFile() == null || !mBgSourceUtils.getCropSourceFile().exists() || mBgSourceUtils.getCropSourceFile().length() <= 0) {
ToastUtils.show("选择的图片文件无效或无法读取");
//clearSelectTempFile();
mBgSourceUtils.clearCropTempFiles();
return;
}
LogUtils.d(TAG, "【选图解析】选图临时文件生成成功:" + mSelectTempFile.getAbsolutePath() + ",大小:" + mSelectTempFile.length() + "bytes");
LogUtils.d(TAG, "【选图解析】选图临时文件生成成功:" + mBgSourceUtils.getCropSourceFile().getAbsolutePath() + ",大小:" + mBgSourceUtils.getCropSourceFile().length() + "bytes");
// 关键修复2同步预览Bean强制绑定「原图路径+压缩图路径」双路径,避免路径错位)
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
ToastUtils.show("预览配置初始化失败");
//clearSelectTempFile();
mBgSourceUtils.clearCropTempFiles();
return;
}
// 绑定原图路径(选图临时文件路径)
previewBean.setBackgroundFilePath(mSelectTempFile.getAbsolutePath());
// 关键强制绑定压缩图路径到BackgroundCrops目录统一存储避免路径错乱
String targetCompressPath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath();
File fPreviewBackgroundScaledCompressFile = new File(targetCompressPath);
File fBackgroundCompressDir =fPreviewBackgroundScaledCompressFile.getParentFile();
if (TextUtils.isEmpty(targetCompressPath) || !targetCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) {
// 兜底生成BackgroundCrops目录下的压缩路径
targetCompressPath = new File(fBackgroundCompressDir, "SelectCompress_" + System.currentTimeMillis() + ".jpg").getAbsolutePath();
LogUtils.d(TAG, "【选图压缩】压缩路径为空/非法,生成兜底路径:" + targetCompressPath);
}
previewBean.setBackgroundScaledCompressFilePath(targetCompressPath);
mBgSourceUtils.saveSettings(); // 立即持久化,避免旋转/退后台丢失路径
String targetCompressPath = mBgSourceUtils.getCropResultFile().getAbsolutePath();
LogUtils.d(TAG, "【选图同步】预览Bean双路径绑定完成");
LogUtils.d(TAG, "→ 原图路径(选图临时文件):" + previewBean.getBackgroundFilePath());
LogUtils.d(TAG, "→ 压缩图路径BackgroundCrops" + previewBean.getBackgroundScaledCompressFilePath());
@@ -1204,7 +1159,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
// 优化使用采样率加载Bitmap避免OOM原有直接decodeFile易导致大图片崩溃
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mSelectTempFile.getAbsolutePath(), options);
BitmapFactory.decodeFile(mBgSourceUtils.getCropSourceFile().getAbsolutePath(), options);
// 计算采样率最大尺寸限制为2048px
int maxSize = 2048;
int sampleSize = 1;
@@ -1214,7 +1169,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存
selectBitmap = BitmapFactory.decodeFile(mSelectTempFile.getAbsolutePath(), options);
selectBitmap = BitmapFactory.decodeFile(mBgSourceUtils.getCropSourceFile().getAbsolutePath(), options);
} catch (Exception e) {
LogUtils.e(TAG, "【选图压缩】Bitmap加载失败" + e.getMessage(), e);
}
@@ -1225,8 +1180,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
} else {
ToastUtils.show("选图后压缩图生成失败");
LogUtils.e(TAG, "【选图压缩失败】无法解析选图文件为Bitmap");
//clearSelectTempFile();
mBgSourceUtils.clearCropTempFiles();
return;
}
@@ -1235,8 +1188,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
if (!compressFile.exists() || compressFile.length() <= 0) {
ToastUtils.show("压缩图生成失败,请重新选择图片");
LogUtils.e(TAG, "【选图压缩失败】压缩图文件无效:" + targetCompressPath);
//clearSelectTempFile();
mBgSourceUtils.clearCropTempFiles();
return;
}
@@ -1258,10 +1209,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
* 处理所有操作取消/失败(统一清理+提示)
*/
private void handleOperationCancelOrFail() {
initPreviewBeanFromFormal();
bvPreviewBackground.reloadPreviewBackground();
LogUtils.d(TAG, "【操作回调】操作取消或失败");
ToastUtils.show("操作已取消");
//clearSelectTempFile(); // 新增:清理选图临时文件
mBgSourceUtils.clearCropTempFiles();
}
// ======================================== 权限回调(转发给工具类处理) ========================================
@@ -1287,92 +1238,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
}
}
/**
* 解析选图Uri到独立的选图临时文件修复避免与拍照临时文件混淆强化可读性校验
*/
private boolean parseUriToSelectTempFile(Uri uri) {
// 清理旧的选图临时文件(避免残留文件干扰解析结果)
if (mSelectTempFile != null && mSelectTempFile.exists()) {
boolean deleteSuccess = mSelectTempFile.delete();
LogUtils.d(TAG, "【选图解析】旧选图临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + mSelectTempFile.getAbsolutePath());
}
InputStream is = null;
FileOutputStream fos = null;
try {
// 1. 打开Uri输入流兼容content:///file:// 等多种Uri格式
is = getContentResolver().openInputStream(uri);
if (is == null) {
LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败Uri" + uri.toString());
return false;
}
// 2. 初始化选图临时文件输出流Java7 手动创建流不依赖try-with-resources
fos = new FileOutputStream(mSelectTempFile);
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();
}
}
// 5. 双重校验临时文件有效性解决Android14+ canRead()假阳性问题)
if (!isFileActuallyReadable(mSelectTempFile)) {
LogUtils.e(TAG, "【选图解析】临时文件存在但无实际读取权限,路径:" + mSelectTempFile.getAbsolutePath());
if (mSelectTempFile.exists()) {
mSelectTempFile.delete(); // 删除不可读文件,避免后续流程异常
}
return false;
}
// 6. 解析成功日志(打印文件信息,便于问题排查)
LogUtils.d(TAG, "【选图解析】Uri解析成功");
LogUtils.d(TAG, "→ 原Uri" + uri.toString());
LogUtils.d(TAG, "→ 目标临时文件:" + mSelectTempFile.getAbsolutePath());
LogUtils.d(TAG, "→ 文件大小:" + mSelectTempFile.length() + " bytes");
return true;
} catch (Exception e) {
// 捕获所有异常IO异常/空指针等),避免崩溃
LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e);
// 异常时清理无效文件,防止残留
if (mSelectTempFile != null && mSelectTempFile.exists()) {
mSelectTempFile.delete();
LogUtils.d(TAG, "【选图解析】异常时清理无效临时文件:" + mSelectTempFile.getAbsolutePath());
}
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());
}
}
}
}
/**
* 精准校验文件是否实际可读取核心用途解决Android14+ 中 File.canRead() 假阳性问题,避免“文件存在但无法读取”)
* 逻辑不依赖canRead()而是通过实际打开文件流读取1字节验证是否真的有读取权限最可靠的校验方式

View File

@@ -3,12 +3,12 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import java.io.BufferedOutputStream;
@@ -18,6 +18,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -28,14 +29,15 @@ public class BackgroundSourceUtils {
public static final String TAG = "BackgroundSourceUtils";
// 裁剪相关常量(统一定义,避免硬编码)
private static final String CROP_TEMP_DIR_NAME = "cache"; // 裁剪缓存目录(基础目录下)
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 COMPRESS_BASE_DIR_NAME = "BackgroundCrops";
private static final String SOURCE_DIR_NAME = "BackgroundSource";
private static final String COMPRESS_DIR_NAME = "BackgroundCompress";
// 1. 静态实例加volatile禁止指令重排保证可见性双重校验锁单例核心
private static volatile BackgroundSourceUtils sInstance;
@@ -48,15 +50,15 @@ public class BackgroundSourceUtils {
// 2. 统一文件目录分两类图片目录→系统公共目录JSON目录→应用外置存储
// 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/
private File fPictureBaseDir; // 图片基础目录
private File fPictureCacheDir; // 裁剪缓存目录(基础目录下/cache
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 cropTempFile; // 裁剪临时文件fPictureCacheDir下
private File cropResultFile; // 裁剪结果文件fBackgroundSourceDir下
private File mCropSourceFile; // 裁剪临时文件fCropCacheDir下
private File mCropResultFile; // 裁剪临时文件fCropCacheDir下
// 3. 私有构造器(加防反射逻辑+初始化所有目录/文件)
private BackgroundSourceUtils(Context context) {
@@ -107,25 +109,26 @@ public class BackgroundSourceUtils {
// 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell
fPictureBaseDir = new File(PICTURE_BASE_DIR);
// 2. 图片存储目录:基础目录下(存储正式/预览原图)
fBackgroundSourceDir = new File(fPictureBaseDir, "BackgroundSource");
fBackgroundSourceDir = new File(fPictureBaseDir, SOURCE_DIR_NAME);
// 3. 裁剪缓存目录:基础目录下/cache所有裁剪操作在此目录
fPictureCacheDir = new File(fPictureBaseDir, CROP_TEMP_DIR_NAME);
fCropCacheDir = new File(fPictureBaseDir, CROP_CACHE_DIR_NAME);
// 4. 新增:压缩图统一存储目录(基础目录下/BackgroundCrops所有压缩图放这里
fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_BASE_DIR_NAME);
fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_DIR_NAME);
// 5. 强制创建所有图片目录(带二次校验+降级兜底)
createDirWithPermission(fPictureBaseDir, "图片基础目录(/Pictures/PowerBell", true);
createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下", true);
createDirWithPermission(fPictureCacheDir, "裁剪缓存目录(基础目录/cache", true);
createDirWithPermission(fBackgroundCompressDir, "压缩图统一存储目录(基础目录/BackgroundCrops", true);
createDirWithPermission(fPictureBaseDir, "图片基础目录(" + PICTURE_BASE_DIR + "");
createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下/" + SOURCE_DIR_NAME + "");
createDirWithPermission(fCropCacheDir, "裁剪缓存目录(基础目录/" + CROP_CACHE_DIR_NAME + "");
createDirWithPermission(fBackgroundCompressDir, "裁剪压缩图存储目录(基础目录/" + COMPRESS_DIR_NAME + "");
// 6. 目录创建后最终校验(确保所有目录已就绪)
validatePictureDirs();
LogUtils.d(TAG, "【图片目录初始化】完成:" +
"基础目录=" + fPictureBaseDir.getAbsolutePath() +
",裁剪缓存目录=" + fPictureCacheDir.getAbsolutePath() +
"压缩图目录=" + fBackgroundCompressDir.getAbsolutePath());
"图片存储目录=" + fBackgroundSourceDir.getAbsolutePath() +
"裁剪缓存目录=" + fCropCacheDir.getAbsolutePath() +
",裁剪压缩图存储目录=" + fBackgroundCompressDir.getAbsolutePath());
}
/**
@@ -137,12 +140,12 @@ public class BackgroundSourceUtils {
fUtilsDir = mContext.getExternalFilesDir(TAG);
if (fUtilsDir == null) {
LogUtils.e(TAG, "【JSON目录】应用外置存储不可用切换到应用内部缓存目录");
fUtilsDir = mContext.getCacheDir();
fUtilsDir = mContext.getDataDir();
}
// 2. 模型文件目录存储JSON配置
fModelDir = new File(fUtilsDir, "ModelDir");
// 强制创建JSON目录带二次校验+降级兜底)
createDirWithPermission(fModelDir, "JSON配置目录应用外置存储", true);
createDirWithPermission(fModelDir, "JSON配置目录应用外置存储");
// 3. 初始化JSON文件对象两份独立文件对应两份Bean实例
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
@@ -155,114 +158,20 @@ public class BackgroundSourceUtils {
* 【核心强化】创建目录并设置权限(适配系统公共目录/Pictures/PowerBell确保实例化时目录就绪
* @param dir 要创建的目录
* @param dirDesc 目录描述(用于日志打印)
* @param needFallback 是否需要降级兜底实例化初期必须为true确保目录可用
*/
private void createDirWithPermission(File dir, String dirDesc, boolean needFallback) {
private void createDirWithPermission(File dir, String dirDesc) {
if (dir == null) {
LogUtils.e(TAG, "【文件管理】创建目录失败目录对象为null描述" + dirDesc + "");
return;
}
// 第一步:主动检测并创建目录(递归创建所有父目录)
boolean isCreated = true;
if (!dir.exists()) {
if (!dir.exists()) {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "不存在,开始创建:" + dir.getAbsolutePath());
isCreated = dir.mkdirs(); // 递归创建所有父目录
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;
}
}
/**
@@ -279,8 +188,8 @@ public class BackgroundSourceUtils {
LogUtils.e(TAG, "【图片目录校验】图片存储目录未就绪:" + fBackgroundSourceDir.getAbsolutePath());
allReady = false;
}
if (!fPictureCacheDir.exists() || !fPictureCacheDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】裁剪缓存目录未就绪:" + fPictureCacheDir.getAbsolutePath());
if (!fCropCacheDir.exists() || !fCropCacheDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】裁剪缓存目录未就绪:" + fCropCacheDir.getAbsolutePath());
allReady = false;
}
if (!fBackgroundCompressDir.exists() || !fBackgroundCompressDir.isDirectory()) {
@@ -298,156 +207,138 @@ public class BackgroundSourceUtils {
* 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录)
*/
private void initAllFiles() {
// 1. 裁剪临时文件(图片基础目录/cache下系统裁剪可读写
cropTempFile = new File(fPictureCacheDir, CROP_TEMP_FILE_NAME);
// 2. 裁剪结果文件(图片存储目录下,最终保存的裁剪图)
cropResultFile = new File(fBackgroundSourceDir, CROP_RESULT_FILE_NAME);
// 1. 裁剪临时文件
mCropSourceFile = new File(fCropCacheDir, CROP_TEMP_FILE_NAME);
// 2. 裁剪结果文件
//cropResultFile = new File(fCropCacheDir, CROP_RESULT_FILE_NAME);
// 3. 初始化时清理旧文件复用FileUtils简化清理逻辑
clearOldFile(cropTempFile, "旧裁剪临时文件(/Pictures/PowerBell/cache");
clearOldFile(cropResultFile, "旧裁剪结果文件(/Pictures/PowerBell/BackgroundSource");
// 新增:清理压缩图目录下的旧文件(避免残留)
clearOldCompressFiles();
LogUtils.d(TAG, "【文件初始化】完成:裁剪临时文件=" + cropTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath());
// 新增:清理压缩图目录下的旧文件(避免残留
clearCropTempFiles();
LogUtils.d(TAG, "【文件初始化】完成。");
}
public boolean createCropFileProviderPath(Uri uri) {
InputStream is = null;
FileOutputStream fos = null;
try {
clearCropTempFiles();
String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis();
mCropSourceFile = new File(fCropCacheDir, newCropFileName + ".jpg");
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + ".jpg");
// 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();
}
}
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
// 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());
}
}
}
}
/**
* 核心优化函数:带原图参数的裁剪路径创建(优先使用原图,不复制)
* 替代原逻辑中"复制原图到BackgroundSource再裁剪"的流程
*/
public File createCropFileProviderPath(File originalImageFile) {
final String TAG = "BackgroundSourceUtils";
Log.d(TAG, "【裁剪优化】createCropFileProviderPath(原图) 触发,优先使用原图路径");
// 核心逻辑1直接使用裁剪前的原图不复制仅校验合法性和权限
if (originalImageFile != null && originalImageFile.exists()
&& originalImageFile.isFile() && originalImageFile.length() > 0) {
// 校验原图目录是否可写(系统裁剪工具需读写权限)
if (isDirectoryWritable(originalImageFile.getParentFile())) {
// 强制开放原图权限(解决跨进程访问问题)
setFileReadWritePermission(originalImageFile);
Log.d(TAG, "【裁剪优化】直接使用原图启动裁剪(无复制):" + originalImageFile.getAbsolutePath());
return originalImageFile; // 直接返回原图,不做任何复制
} else {
Log.w(TAG, "【裁剪优化】原图目录不可写,切换到临时文件兜底");
}
} else {
Log.w(TAG, "【裁剪优化】原图无效(空/不存在/0大小切换到临时文件兜底");
}
// 兜底逻辑仅原图不可用时触发避免多复制使用应用缓存目录不涉及BackgroundSource
File cacheDir = mContext.getCacheDir(); // 应用内部缓存,无需额外权限
if (isDirectoryWritable(cacheDir)) {
try {
File cropTempFile = new File(cacheDir, "crop_temp_" + System.currentTimeMillis() + ".jpg");
// 清理旧临时文件(避免堆积)
if (cropTempFile.exists()) {
cropTempFile.delete();
}
cropTempFile.createNewFile();
setFileReadWritePermission(cropTempFile);
Log.d(TAG, "【裁剪优化】原图不可用,使用缓存临时文件:" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
Log.e(TAG, "【裁剪优化】创建缓存临时文件失败:" + e.getMessage(), e);
}
}
// 终极兜底:公共图片缓存目录(极端情况)
File publicCacheDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Cache");
if (isDirectoryWritable(publicCacheDir)) {
try {
if (!publicCacheDir.exists()) {
publicCacheDir.mkdirs();
}
File cropTempFile = new File(publicCacheDir, "crop_temp_" + System.currentTimeMillis() + ".jpg");
if (cropTempFile.exists()) {
cropTempFile.delete();
}
cropTempFile.createNewFile();
setFileReadWritePermission(cropTempFile);
Log.w(TAG, "【裁剪优化】应用缓存不可用,使用公共图片缓存:" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
Log.e(TAG, "【裁剪优化】创建公共临时文件失败:" + e.getMessage(), e);
}
}
Log.e(TAG, "【裁剪优化】所有裁剪路径创建失败,裁剪功能不可用");
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); // 无原图时走兜底逻辑
}
// File createFileProviderPath(File originalImageFile) {
// Log.d(TAG, "【裁剪优化】createCropFileProviderPath(原图) 触发,优先使用原图路径");
//
// // 核心逻辑1直接使用裁剪前的原图不复制仅校验合法性和权限
// if (originalImageFile != null && originalImageFile.exists()
// && originalImageFile.isFile() && originalImageFile.length() > 0) {
// // 校验原图目录是否可写(系统裁剪工具需读写权限)
// if (isDirectoryWritable(originalImageFile.getParentFile())) {
// // 强制开放原图权限(解决跨进程访问问题)
// //setFileReadWritePermission(originalImageFile);
// Log.d(TAG, "【裁剪优化】直接使用原图启动裁剪(无复制):" + originalImageFile.getAbsolutePath());
// return originalImageFile; // 直接返回原图,不做任何复制
// } else {
// Log.w(TAG, "【裁剪优化】原图目录不可写,切换到临时文件兜底");
// }
// } else {
// Log.w(TAG, "【裁剪优化】原图无效(空/不存在/0大小切换到临时文件兜底");
// }
// Log.e(TAG, "【裁剪优化】所有裁剪路径创建失败,裁剪功能不可用");
// return null;
// }
// 辅助方法:校验目录是否可写(新增,保障裁剪权限)
private boolean isDirectoryWritable(File dir) {
if (dir == null) return false;
// 目录存在且可写,或目录不存在但能创建
return (dir.exists() && dir.isDirectory() && dir.canWrite())
|| (!dir.exists() && dir.mkdirs());
}
// 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);
}
// 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实例
@@ -567,12 +458,12 @@ public class BackgroundSourceUtils {
return fBackgroundCompressDir.getAbsolutePath();
}
public File getCropTempFile() {
return cropTempFile;
public File getCropSourceFile() {
return mCropSourceFile;
}
public File getCropResultFile() {
return cropResultFile;
return mCropResultFile;
}
public String getFileProviderAuthority() {
@@ -680,69 +571,69 @@ public class BackgroundSourceUtils {
* 工具方法:递归设置目录及子目录/文件的读写权限(适配系统公共目录/Pictures/PowerBell
* @param dir 要设置权限的目录
*/
private void setDirPermissionsRecursively(File dir) {
if (dir == null || !dir.exists()) {
String dirPath = (dir != null) ? dir.getAbsolutePath() : "null";
LogUtils.d(TAG, "【权限管理】目录无效,无需设置权限:" + dirPath);
return;
}
try {
// 设置目录权限(允许所有用户读写,系统裁剪应用/预览功能必需)
dir.setReadable(true, false);
dir.setWritable(true, false);
dir.setExecutable(false, false);
LogUtils.d(TAG, "【权限管理】目录权限设置完成:路径=" + dir.getAbsolutePath() + ",可写=" + dir.canWrite() + ",可读=" + dir.canRead());
// 递归处理子目录和文件Java7 普通for循环兼容语法
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
setDirPermissionsRecursively(file);
} else {
// 设置文件权限(与目录一致,确保可读写)
file.setReadable(true, false);
file.setWritable(true, false);
file.setExecutable(false, false);
// 裁剪/压缩/预览相关文件单独打印日志
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());
}
}
}
}
} catch (SecurityException e) {
LogUtils.e(TAG, "【权限管理】设置目录权限失败(系统禁止):" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
ToastUtils.show("目录权限设置失败,请授予应用存储权限");
} catch (Exception e) {
LogUtils.e(TAG, "【权限管理】设置目录权限异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
}
}
// private void setDirPermissionsRecursively(File dir) {
// if (dir == null || !dir.exists()) {
// String dirPath = (dir != null) ? dir.getAbsolutePath() : "null";
// LogUtils.d(TAG, "【权限管理】目录无效,无需设置权限:" + dirPath);
// return;
// }
// try {
// // 设置目录权限(允许所有用户读写,系统裁剪应用/预览功能必需)
// dir.setReadable(true, false);
// dir.setWritable(true, false);
// dir.setExecutable(false, false);
//
// LogUtils.d(TAG, "【权限管理】目录权限设置完成:路径=" + dir.getAbsolutePath() + ",可写=" + dir.canWrite() + ",可读=" + dir.canRead());
//
// // 递归处理子目录和文件Java7 普通for循环兼容语法
// File[] files = dir.listFiles();
// if (files != null && files.length > 0) {
// for (int i = 0; i < files.length; i++) {
// File file = files[i];
// if (file.isDirectory()) {
// setDirPermissionsRecursively(file);
// } else {
// // 设置文件权限(与目录一致,确保可读写)
// file.setReadable(true, false);
// file.setWritable(true, false);
// file.setExecutable(false, false);
// // 裁剪/压缩/预览相关文件单独打印日志
// 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());
// }
// }
// }
// }
// } catch (SecurityException e) {
// LogUtils.e(TAG, "【权限管理】设置目录权限失败(系统禁止):" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
// ToastUtils.show("目录权限设置失败,请授予应用存储权限");
// } catch (Exception e) {
// LogUtils.e(TAG, "【权限管理】设置目录权限异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
// }
// }
/**
* 工具方法:设置单个文件权限(确保系统裁剪应用/预览功能可读写,适配/Pictures/PowerBell目录
* 【关键调整】public修饰适配BackgroundSettingsActivity的外部调用
* @param file 要设置权限的文件
*/
public void setFilePermissions(File file) {
if (file == null || !file.exists()) {
LogUtils.d(TAG, "【权限管理】文件无效,无需设置权限:" + (file != null ? file.getAbsolutePath() : "null"));
return;
}
try {
// 核心:允许所有用户读写(系统裁剪应用非本应用进程,需开放权限)
file.setReadable(true, false);
file.setWritable(true, false);
file.setExecutable(false, false);
LogUtils.d(TAG, "【权限管理】文件权限设置完成(/Pictures/PowerBell下路径=" + file.getAbsolutePath() + ",可写=" + file.canWrite() + ",可读=" + file.canRead());
} catch (Exception e) {
LogUtils.e(TAG, "【权限管理】设置文件权限失败:" + file.getAbsolutePath() + ",错误:" + e.getMessage(), e);
}
}
// public void setFilePermissions(File file) {
// if (file == null || !file.exists()) {
// LogUtils.d(TAG, "【权限管理】文件无效,无需设置权限:" + (file != null ? file.getAbsolutePath() : "null"));
// return;
// }
// try {
// // 核心:允许所有用户读写(系统裁剪应用非本应用进程,需开放权限)
// file.setReadable(true, false);
// file.setWritable(true, false);
// file.setExecutable(false, false);
// LogUtils.d(TAG, "【权限管理】文件权限设置完成(/Pictures/PowerBell下路径=" + file.getAbsolutePath() + ",可写=" + file.canWrite() + ",可读=" + file.canRead());
// } catch (Exception e) {
// LogUtils.e(TAG, "【权限管理】设置文件权限失败:" + file.getAbsolutePath() + ",错误:" + e.getMessage(), e);
// }
// }
/**
* 工具方法:清理旧文件(避免文件锁定/残留,适配系统公共目录)【内部私有,不对外暴露】
@@ -754,34 +645,20 @@ public class BackgroundSourceUtils {
return;
}
if (file.exists()) {
// 先设置文件为可写(避免系统公共目录下文件只读导致删除失败)
file.setWritable(true, false);
boolean deleteSuccess = file.delete();
LogUtils.d(TAG, "【文件管理】清理" + fileDesc + "" + (deleteSuccess ? "成功" : "失败") + ",路径:" + file.getAbsolutePath());
// 若删除失败,标记为退出时删除(兼容文件锁定场景)
if (!deleteSuccess) {
file.deleteOnExit();
LogUtils.w(TAG, "【文件管理】" + fileDesc + "删除失败,标记为退出时自动删除");
}
file.delete();
LogUtils.w(TAG, "【文件管理】" + fileDesc + "已删除");
}
}
/**
* 新增:清理压缩图目录下的旧文件(避免残留,初始化时调用)
*/
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");
void clearCropTempFiles() {
for(File file : fCropCacheDir.listFiles()){
clearOldFile(file, "旧裁剪缓存文件(" + file.getAbsolutePath() + ")");
}
mCropSourceFile = null;
mCropResultFile = null;
}
/**
@@ -790,63 +667,38 @@ public class BackgroundSourceUtils {
* @param dir 要验证的目录
* @return true=实际可写false=实际不可写
*/
private boolean isDirActuallyWritable(File dir) {
if (dir == null || !dir.exists() || !dir.isDirectory()) {
return false;
}
// 创建临时空文件(随机文件名,避免冲突)
File testFile = new File(dir, "test_write_" + System.currentTimeMillis() + ".tmp");
try {
boolean createSuccess = testFile.createNewFile();
if (createSuccess) {
boolean canWrite = testFile.canWrite();
boolean canRead = testFile.canRead();
testFile.delete(); // 删除临时文件,不占用空间
LogUtils.d(TAG, "【权限校验】目录实际写入校验(/Pictures/PowerBell下" + dir.getAbsolutePath() + ",创建成功=" + createSuccess + ",可写=" + canWrite + ",可读=" + canRead + ",结果=" + (canWrite ? "通过" : "失败"));
return canWrite;
} else {
LogUtils.d(TAG, "【权限校验】目录实际写入校验失败:" + dir.getAbsolutePath() + "创建临时文件失败Permission denied");
return false;
}
} catch (IOException e) {
LogUtils.e(TAG, "【权限校验】目录实际写入校验异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
return false;
}
}
// private boolean isDirActuallyWritable(File dir) {
// if (dir == null || !dir.exists() || !dir.isDirectory()) {
// return false;
// }
// // 创建临时空文件(随机文件名,避免冲突)
// File testFile = new File(dir, "test_write_" + System.currentTimeMillis() + ".tmp");
// try {
// boolean createSuccess = testFile.createNewFile();
// if (createSuccess) {
// boolean canWrite = testFile.canWrite();
// boolean canRead = testFile.canRead();
// testFile.delete(); // 删除临时文件,不占用空间
// LogUtils.d(TAG, "【权限校验】目录实际写入校验(/Pictures/PowerBell下" + dir.getAbsolutePath() + ",创建成功=" + createSuccess + ",可写=" + canWrite + ",可读=" + canRead + ",结果=" + (canWrite ? "通过" : "失败"));
// return canWrite;
// } else {
// LogUtils.d(TAG, "【权限校验】目录实际写入校验失败:" + dir.getAbsolutePath() + "创建临时文件失败Permission denied");
// return false;
// }
// } catch (IOException e) {
// LogUtils.e(TAG, "【权限校验】目录实际写入校验异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
// return false;
// }
// }
/**
* 对外接口清理指定旧文件适配BackgroundSettingsActivity调用支持/Pictures/PowerBell目录
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
public void clearOldFileByExternal(File file, String fileDesc) {
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下");
}
// public void clearOldFileByExternal(File file, String fileDesc) {
// clearOldFile(file, fileDesc); // 调用内部private方法复用逻辑
// }
/**
* 适配原调用mBgSourceUtils.copyFile(new File(""), parentDir)
@@ -864,7 +716,7 @@ public class BackgroundSourceUtils {
}
// 若target是文件取其父目录若本身是目录直接创建实例化时已创建此处二次确认
File targetDir = target.isFile() ? target.getParentFile() : target;
createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下", false);
createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下");
LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath());
return true;
}
@@ -888,7 +740,7 @@ public class BackgroundSourceUtils {
String cachePath = mContext.getCacheDir().getAbsolutePath();
if (!TextUtils.isEmpty(publicPicturePath)) {
if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_BASE_DIR_NAME)) {
if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_DIR_NAME)) {
return "系统公共图片目录(/Pictures/PowerBell/BackgroundCrops压缩图统一存储目录"; // 新增压缩图目录描述
} else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) {
return "系统公共图片目录(/Pictures/PowerBell图片存储/裁剪目录)";
@@ -1073,9 +925,9 @@ public class BackgroundSourceUtils {
}
// 清理旧的压缩文件(避免文件残留)
if (compressFile.exists()) {
clearOldFileByExternal(compressFile, "旧压缩文件BackgroundCrops");
}
// if (compressFile.exists()) {
// clearOldFileByExternal(compressFile, "旧压缩文件BackgroundCrops");
// }
compressFile.createNewFile();
// 写入压缩图质量80平衡清晰度和内存