文件管理类规范划分
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Nov 30 23:19:16 GMT 2025
|
||||
#Mon Dec 01 00:19:18 GMT 2025
|
||||
stageCount=13
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.12
|
||||
buildCount=33
|
||||
buildCount=36
|
||||
baseBetaVersion=15.11.13
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
initEnv();
|
||||
|
||||
mContext = context;
|
||||
mBackgroundPictureUtils = ((BackgroundSettingsActivity)context).mBackgroundSourceUtils;
|
||||
mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
|
||||
|
||||
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
|
||||
copyAndViewRecivePicture(imageView);
|
||||
@@ -95,7 +95,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
|
||||
|
||||
File fSrcImage = new File(szSrcImage);
|
||||
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
|
||||
File mfPreReceivedPhoto = new File(activity.mBackgroundSourceUtils.getBackgroundSourceDirPath(), mszPreReceivedFileName);
|
||||
File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
|
||||
// 复制源图片到剪裁文件
|
||||
try {
|
||||
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.powerbell.BuildConfig;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import java.io.File;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/18 12:07:20
|
||||
* @Describe 背景图片工具集(修复单例模式,线程安全+数据流转正常)
|
||||
* @Describe 背景图片工具集(全量文件管理+裁剪FileProvider路径适配,线程安全+数据流转正常)
|
||||
* 核心能力:
|
||||
* 1. 统一管理所有文件路径(背景图/裁剪临时文件/压缩图)
|
||||
* 2. 为系统裁剪应用创建可读写的FileProvider路径(适配Android14+ MIUI)
|
||||
* 3. 替代BackgroundSettingsActivity的所有文件操作逻辑
|
||||
*/
|
||||
public class BackgroundSourceUtils {
|
||||
|
||||
public static final String TAG = "BackgroundPictureUtils";
|
||||
// 裁剪相关常量(统一定义,避免硬编码)
|
||||
private static final String CROP_TEMP_DIR_NAME = "CropTemp"; // 裁剪临时目录(FileProvider适配)
|
||||
private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; // 裁剪输入临时文件
|
||||
private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; // 裁剪输出结果文件
|
||||
private static final String CROP_FALLBACK_DIR_NAME = "CropFallback"; // 裁剪兜底目录
|
||||
private static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容
|
||||
|
||||
// 1. 静态实例加volatile,禁止指令重排,保证可见性(双重校验锁单例核心)
|
||||
private static volatile BackgroundSourceUtils sInstance;
|
||||
@@ -22,13 +36,17 @@ public class BackgroundSourceUtils {
|
||||
private BackgroundBean currentBackgroundBean;
|
||||
private File previewBackgroundBeanFile;
|
||||
private BackgroundBean previewBackgroundBean;
|
||||
// 应用外部存储文件夹路径(按功能划分目录,避免路径混乱)
|
||||
private File fUtilsDir;
|
||||
private File fModelDir;
|
||||
// 背景图片源文件目录(存储正式/预览图片)
|
||||
private File fBackgroundSourceDir;
|
||||
|
||||
// 2. 私有构造器(加防反射逻辑+初始化目录)
|
||||
// 2. 统一文件目录(全量文件管理,替代Activity中的目录变量)
|
||||
private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundPictureUtils)
|
||||
private File fModelDir; // 模型文件目录(存储BackgroundBean的JSON文件)
|
||||
private File fBackgroundSourceDir; // 背景图片源目录(存储正式/预览图片)
|
||||
private File fCropTempDir; // 裁剪临时目录(FileProvider适配路径,系统裁剪应用可读写)
|
||||
private File fCropFallbackDir; // 裁剪兜底目录(应用私有外部目录失败时使用)
|
||||
private File cropTempFile; // 裁剪临时文件(系统裁剪应用写入目标)
|
||||
private File cropResultFile; // 裁剪结果文件(裁剪后保存的最终文件)
|
||||
|
||||
// 3. 私有构造器(加防反射逻辑+初始化所有目录/文件)
|
||||
private BackgroundSourceUtils(Context context) {
|
||||
// 防反射破坏:若已有实例,抛异常阻止重复创建
|
||||
if (sInstance != null) {
|
||||
@@ -36,13 +54,15 @@ public class BackgroundSourceUtils {
|
||||
}
|
||||
// 上下文用Application Context,避免Activity内存泄漏
|
||||
this.mContext = context.getApplicationContext();
|
||||
// 初始化目录(按功能划分,确保目录存在)
|
||||
initDirs();
|
||||
// 初始化所有目录(文件管理核心:统一创建+权限设置)
|
||||
initAllDirs();
|
||||
// 初始化所有文件(裁剪临时文件/结果文件等)
|
||||
initAllFiles();
|
||||
// 加载配置(正式/预览Bean)
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
// 3. 双重校验锁单例(线程安全,高效,支持多线程并发调用)
|
||||
// 4. 双重校验锁单例(线程安全,高效,支持多线程并发调用)
|
||||
public static BackgroundSourceUtils getInstance(Context context) {
|
||||
// 第一重校验:避免每次调用都加锁(提升效率)
|
||||
if (sInstance == null) {
|
||||
@@ -58,33 +78,110 @@ public class BackgroundSourceUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有目录(确保目录存在,避免文件操作失败)
|
||||
* 初始化所有文件目录(统一管理,替代Activity中的目录初始化逻辑)
|
||||
* 包含:工具类根目录、模型目录、背景图目录、裁剪临时目录、裁剪兜底目录
|
||||
*/
|
||||
private void initDirs() {
|
||||
// 工具类根目录(外部存储:/Android/data/包名/files/BackgroundPictureUtils)
|
||||
private void initAllDirs() {
|
||||
// 1. 工具类根目录(应用外部存储:/Android/data/包名/files/BackgroundPictureUtils)
|
||||
fUtilsDir = mContext.getExternalFilesDir(TAG);
|
||||
if (fUtilsDir == null) {
|
||||
LogUtils.e(TAG, "外部存储不可用,无法初始化目录");
|
||||
return;
|
||||
}
|
||||
// 模型文件目录(存储BackgroundBean的JSON文件)
|
||||
fModelDir = new File(fUtilsDir, "ModelDir");
|
||||
// 背景图片源目录(存储正式/预览图片文件)
|
||||
fBackgroundSourceDir = new File(fUtilsDir, "BackgroundSource");
|
||||
|
||||
// 递归创建所有目录(确保目录存在)
|
||||
if (!fModelDir.exists()) {
|
||||
fModelDir.mkdirs();
|
||||
LogUtils.d(TAG, "创建模型文件目录:" + fModelDir.getAbsolutePath());
|
||||
}
|
||||
if (!fBackgroundSourceDir.exists()) {
|
||||
fBackgroundSourceDir.mkdirs();
|
||||
LogUtils.d(TAG, "创建背景图片目录:" + fBackgroundSourceDir.getAbsolutePath());
|
||||
LogUtils.e(TAG, "【文件管理】应用外部存储不可用,切换到应用内部缓存目录");
|
||||
fUtilsDir = mContext.getCacheDir(); // 极端兜底:应用内部缓存目录
|
||||
}
|
||||
|
||||
// 初始化Bean文件对象(存储JSON配置)
|
||||
// 2. 子目录初始化(按功能划分,确保目录存在并设置权限)
|
||||
fModelDir = new File(fUtilsDir, "ModelDir"); // 模型文件目录(JSON配置)
|
||||
fBackgroundSourceDir = new File(fUtilsDir, "BackgroundSource"); // 背景图片目录
|
||||
fCropTempDir = new File(fUtilsDir, CROP_TEMP_DIR_NAME); // 裁剪临时目录(FileProvider适配路径)
|
||||
fCropFallbackDir = new File(fUtilsDir, CROP_FALLBACK_DIR_NAME); // 裁剪兜底目录
|
||||
|
||||
// 3. 递归创建所有目录(确保目录存在,Android14+ 必需)
|
||||
createDirWithPermission(fModelDir, "模型文件目录");
|
||||
createDirWithPermission(fBackgroundSourceDir, "背景图片目录");
|
||||
createDirWithPermission(fCropTempDir, "裁剪临时目录(FileProvider适配)");
|
||||
createDirWithPermission(fCropFallbackDir, "裁剪兜底目录");
|
||||
|
||||
// 4. 初始化Bean文件对象(存储JSON配置)
|
||||
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
|
||||
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
|
||||
|
||||
LogUtils.d(TAG, "【文件管理】所有目录初始化完成:根目录=" + fUtilsDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有文件(统一管理,替代Activity中的文件初始化逻辑)
|
||||
* 包含:裁剪临时文件、裁剪结果文件
|
||||
*/
|
||||
private void initAllFiles() {
|
||||
// 1. 裁剪临时文件(系统裁剪应用写入路径,优先裁剪临时目录)
|
||||
cropTempFile = new File(fCropTempDir, CROP_TEMP_FILE_NAME);
|
||||
// 2. 裁剪结果文件(裁剪后保存的最终文件,存入背景图片目录)
|
||||
cropResultFile = new File(fBackgroundSourceDir, CROP_RESULT_FILE_NAME);
|
||||
|
||||
// 3. 初始化时清理旧文件(避免文件锁定/权限残留)
|
||||
clearOldFile(cropTempFile, "旧裁剪临时文件");
|
||||
clearOldFile(cropResultFile, "旧裁剪结果文件");
|
||||
|
||||
LogUtils.d(TAG, "【文件管理】所有文件初始化完成:裁剪临时文件=" + cropTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心函数:为系统裁剪应用创建可读写的FileProvider路径(满足需求)
|
||||
* 功能:确保裁剪临时目录(fCropTempDir)可读写,返回裁剪临时文件(系统裁剪应用写入目标)
|
||||
* 适配:Android14+、MIUI,解决Permission denied问题,确保系统裁剪应用能正常写入
|
||||
* @return 裁剪临时文件(File),系统裁剪应用可读写,路径已适配FileProvider
|
||||
*/
|
||||
public File createCropFileProviderPath() {
|
||||
LogUtils.d(TAG, "【裁剪路径】createCropFileProviderPath 触发,创建系统裁剪可读写路径");
|
||||
|
||||
// 1. 优先尝试裁剪临时目录(FileProvider适配路径)
|
||||
if (isDirActuallyWritable(fCropTempDir)) {
|
||||
try {
|
||||
// 重新初始化裁剪临时文件(先删后建,避免文件锁定)
|
||||
clearOldFile(cropTempFile, "裁剪临时文件(重新初始化)");
|
||||
cropTempFile.createNewFile();
|
||||
// 强制设置文件权限(系统裁剪应用必需:允许所有用户读写)
|
||||
setFilePermissions(cropTempFile);
|
||||
LogUtils.d(TAG, "【裁剪路径】系统裁剪可读写路径创建成功(裁剪临时目录):" + cropTempFile.getAbsolutePath());
|
||||
return cropTempFile;
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【裁剪路径】裁剪临时目录创建文件失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 兜底1:裁剪临时目录失败,切换到裁剪兜底目录
|
||||
if (isDirActuallyWritable(fCropFallbackDir)) {
|
||||
try {
|
||||
cropTempFile = new File(fCropFallbackDir, CROP_TEMP_FILE_NAME);
|
||||
clearOldFile(cropTempFile, "裁剪临时文件(兜底目录)");
|
||||
cropTempFile.createNewFile();
|
||||
setFilePermissions(cropTempFile);
|
||||
LogUtils.w(TAG, "【裁剪路径】裁剪临时目录失败,切换到兜底目录创建成功:" + cropTempFile.getAbsolutePath());
|
||||
return cropTempFile;
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【裁剪路径】裁剪兜底目录创建文件失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 终极兜底:应用内部缓存目录(确保不崩溃)
|
||||
File cacheDir = mContext.getCacheDir();
|
||||
if (isDirActuallyWritable(cacheDir)) {
|
||||
try {
|
||||
cropTempFile = new File(cacheDir, CROP_TEMP_FILE_NAME);
|
||||
clearOldFile(cropTempFile, "裁剪临时文件(终极兜底)");
|
||||
cropTempFile.createNewFile();
|
||||
setFilePermissions(cropTempFile);
|
||||
LogUtils.w(TAG, "【裁剪路径】应用外部目录全部失败,终极兜底到缓存目录:" + cropTempFile.getAbsolutePath());
|
||||
return cropTempFile;
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【裁剪路径】终极兜底目录创建文件失败:" + e.getMessage(), e);
|
||||
ToastUtils.show("裁剪路径创建失败,请重启应用");
|
||||
}
|
||||
}
|
||||
|
||||
// 极端情况:所有目录均失败(返回null,上层需处理)
|
||||
LogUtils.e(TAG, "【裁剪路径】所有目录均无法创建裁剪文件,系统裁剪功能不可用");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +194,7 @@ public class BackgroundSourceUtils {
|
||||
if (currentBackgroundBean == null) {
|
||||
currentBackgroundBean = new BackgroundBean();
|
||||
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
|
||||
LogUtils.d(TAG, "正式背景Bean不存在,创建默认Bean");
|
||||
LogUtils.d(TAG, "【配置管理】正式背景Bean不存在,创建默认Bean");
|
||||
}
|
||||
|
||||
// 加载预览Bean
|
||||
@@ -105,7 +202,7 @@ public class BackgroundSourceUtils {
|
||||
if (previewBackgroundBean == null) {
|
||||
previewBackgroundBean = new BackgroundBean();
|
||||
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
|
||||
LogUtils.d(TAG, "预览背景Bean不存在,创建默认Bean");
|
||||
LogUtils.d(TAG, "【配置管理】预览背景Bean不存在,创建默认Bean");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +226,7 @@ public class BackgroundSourceUtils {
|
||||
public String getCurrentBackgroundFilePath() {
|
||||
loadSettings(); // 加载最新配置,避免数据滞后
|
||||
File file = new File(fBackgroundSourceDir, currentBackgroundBean.getBackgroundFileName());
|
||||
LogUtils.d(TAG, "正式背景路径:" + file.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath());
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
@@ -139,7 +236,7 @@ public class BackgroundSourceUtils {
|
||||
public String getPreviewBackgroundFilePath() {
|
||||
loadSettings(); // 加载最新配置,避免数据滞后
|
||||
File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundFileName());
|
||||
LogUtils.d(TAG, "预览背景路径:" + file.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath());
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
@@ -149,7 +246,7 @@ public class BackgroundSourceUtils {
|
||||
public String getPreviewBackgroundScaledCompressFilePath() {
|
||||
loadSettings(); // 加载最新配置,避免数据滞后
|
||||
File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundScaledCompressFileName());
|
||||
LogUtils.d(TAG, "预览压缩背景路径:" + file.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【路径管理】预览压缩背景路径:" + file.getAbsolutePath());
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
@@ -159,7 +256,7 @@ public class BackgroundSourceUtils {
|
||||
public void saveSettings() {
|
||||
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
|
||||
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
|
||||
LogUtils.d(TAG, "配置保存成功:正式Bean=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览Bean=" + previewBackgroundBeanFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【配置管理】配置保存成功:正式Bean=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览Bean=" + previewBackgroundBeanFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,6 +266,27 @@ public class BackgroundSourceUtils {
|
||||
return fBackgroundSourceDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取裁剪临时文件(对外提供,Activity中用于传递给系统裁剪应用)
|
||||
*/
|
||||
public File getCropTempFile() {
|
||||
return cropTempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取裁剪结果文件(对外提供,Activity中用于获取裁剪后的图片)
|
||||
*/
|
||||
public File getCropResultFile() {
|
||||
return cropResultFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取FileProvider授权Authority(多包名兼容,对外提供给Activity)
|
||||
*/
|
||||
public String getFileProviderAuthority() {
|
||||
return FILE_PROVIDER_AUTHORITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片到预览Bean(核心修复:解决文件名覆盖+路径无效问题)
|
||||
* @param sourceFile 源图片文件(非空,必须存在)
|
||||
@@ -178,15 +296,14 @@ public class BackgroundSourceUtils {
|
||||
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
|
||||
// 校验源文件合法性
|
||||
if (sourceFile == null || !sourceFile.exists() || !sourceFile.isFile()) {
|
||||
LogUtils.e(TAG, "源文件无效:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
|
||||
LogUtils.e(TAG, "【文件管理】源文件无效:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
|
||||
ToastUtils.show("源图片文件无效");
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
|
||||
// 确保背景目录存在(防止首次使用时目录未创建)
|
||||
if (!fBackgroundSourceDir.exists()) {
|
||||
fBackgroundSourceDir.mkdirs();
|
||||
LogUtils.d(TAG, "背景目录不存在,自动创建:" + fBackgroundSourceDir.getAbsolutePath());
|
||||
createDirWithPermission(fBackgroundSourceDir, "背景图片目录(saveFileToPreviewBean)");
|
||||
}
|
||||
|
||||
// 生成唯一文件名(基于源文件后缀,避免重复)
|
||||
@@ -196,7 +313,7 @@ public class BackgroundSourceUtils {
|
||||
// 复制源文件到预览目录(确保图片实际保存成功)
|
||||
boolean copySuccess = FileUtils.copyFile(sourceFile, previewBackgroundFile);
|
||||
if (!copySuccess) {
|
||||
LogUtils.e(TAG, "图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + " → " + previewBackgroundFile.getAbsolutePath());
|
||||
LogUtils.e(TAG, "【文件管理】图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + " → " + previewBackgroundFile.getAbsolutePath());
|
||||
ToastUtils.show("预览图片保存失败");
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
@@ -206,12 +323,13 @@ public class BackgroundSourceUtils {
|
||||
previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 正确赋值:唯一文件名
|
||||
previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_" + previewBackgroundFile.getName()); // 压缩文件名(前缀标识)
|
||||
previewBackgroundBean.setBackgroundFileInfo(fileInfo); // 正确赋值:附加信息(Uri)
|
||||
previewBackgroundBean.setIsUseBackgroundFile(true); // 标记使用背景图,确保BackgroundView加载
|
||||
previewBackgroundBean.setIsUseBackgroundFile(true); // 标记使用背景图
|
||||
previewBackgroundBean.setIsUseScaledCompress(true); // 启用压缩图,提升预览加载速度
|
||||
previewBackgroundBean.setBackgroundWidth(100); // 默认宽高比1:1(可根据需求调整)
|
||||
previewBackgroundBean.setBackgroundHeight(100);
|
||||
saveSettings(); // 持久化保存预览Bean
|
||||
|
||||
LogUtils.d(TAG, "预览图片保存成功:" + previewBackgroundFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "【文件管理】预览图片保存成功:" + previewBackgroundFile.getAbsolutePath());
|
||||
ToastUtils.show("预览图片加载成功");
|
||||
return previewBackgroundBean;
|
||||
}
|
||||
@@ -232,7 +350,7 @@ public class BackgroundSourceUtils {
|
||||
currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor());
|
||||
|
||||
saveSettings(); // 持久化保存正式Bean
|
||||
LogUtils.d(TAG, "预览背景提交成功:正式背景更新为预览背景");
|
||||
LogUtils.d(TAG, "【配置管理】预览背景提交成功:正式背景更新为预览背景");
|
||||
ToastUtils.show("背景图片应用成功");
|
||||
}
|
||||
|
||||
@@ -252,7 +370,238 @@ public class BackgroundSourceUtils {
|
||||
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
|
||||
|
||||
saveSettings(); // 持久化保存预览Bean
|
||||
LogUtils.d(TAG, "正式背景同步到预览:预览背景更新为当前正式背景");
|
||||
LogUtils.d(TAG, "【配置管理】正式背景同步到预览:预览背景更新为当前正式背景");
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:创建目录并设置权限(确保目录可读写,适配Android14+)
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:递归设置目录及子目录/文件的读写权限(适配Android全版本)
|
||||
* @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());
|
||||
|
||||
// 递归处理子目录和文件
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
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)) {
|
||||
LogUtils.d(TAG, "【权限管理】裁剪文件权限设置:文件名=" + file.getName() + ",可写=" + file.canWrite());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "【权限管理】设置目录权限失败(系统禁止):" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【权限管理】设置目录权限异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:设置单个文件权限(确保系统裁剪应用可读写)
|
||||
* 【关键调整】修改为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, "【权限管理】文件权限设置完成:路径=" + file.getAbsolutePath() + ",可写=" + file.canWrite() + ",可读=" + file.canRead());
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【权限管理】设置文件权限失败:" + file.getAbsolutePath() + ",错误:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:清理旧文件(避免文件锁定/残留)
|
||||
* @param file 要清理的文件
|
||||
* @param fileDesc 文件描述(用于日志打印)
|
||||
*/
|
||||
/**
|
||||
* 工具方法:清理旧文件(避免文件锁定/残留)【内部私有,不对外暴露】
|
||||
* @param file 要清理的文件
|
||||
* @param fileDesc 文件描述(用于日志打印)
|
||||
*/
|
||||
private void clearOldFile(File file, String fileDesc) {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
if (file.exists()) {
|
||||
boolean deleteSuccess = file.delete();
|
||||
LogUtils.d(TAG, "【文件管理】清理" + fileDesc + ":" + (deleteSuccess ? "成功" : "失败") + ",路径:" + file.getAbsolutePath());
|
||||
// 若删除失败,标记为退出时删除(兼容文件锁定场景)
|
||||
if (!deleteSuccess) {
|
||||
file.deleteOnExit();
|
||||
LogUtils.w(TAG, "【文件管理】" + fileDesc + "删除失败,标记为退出时自动删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:验证目录实际写入能力(解决Android14+ canWrite()假阳性问题)
|
||||
* 原理:通过创建临时空文件并删除,验证目录是否真的可写
|
||||
* @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();
|
||||
testFile.delete(); // 删除临时文件,不占用空间
|
||||
LogUtils.d(TAG, "【权限校验】目录实际写入校验:" + dir.getAbsolutePath() + ",结果=" + (canWrite ? "通过" : "失败"));
|
||||
return canWrite;
|
||||
} else {
|
||||
LogUtils.d(TAG, "【权限校验】目录实际写入校验失败:" + dir.getAbsolutePath() + ",创建临时文件失败");
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【权限校验】目录实际写入校验异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:复制文件(适配大文件,避免OOM)
|
||||
* 【关键优化】兼容源文件为空的场景(适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用)
|
||||
* @param source 源文件(可为空,为空时仅创建目标目录)
|
||||
* @param target 目标文件/目录(若源文件为空,target视为目录并创建)
|
||||
* @return true=复制/创建成功,false=失败
|
||||
*/
|
||||
public boolean copyFile(File source, File target) {
|
||||
// 场景1:源文件为空 → 仅创建目标目录(适配Activity的目录创建调用)
|
||||
if (source == null || (source.exists() && source.length() <= 0)) {
|
||||
if (target == null) {
|
||||
LogUtils.e(TAG, "【文件管理】目录创建失败:目标目录对象为null");
|
||||
return false;
|
||||
}
|
||||
// 若target是文件,取其父目录;若本身是目录,直接创建
|
||||
File targetDir = target.isFile() ? target.getParentFile() : target;
|
||||
createDirWithPermission(targetDir, "空源文件场景-目录创建");
|
||||
LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath());
|
||||
return true;
|
||||
}
|
||||
|
||||
// 场景2:正常文件复制(源文件非空且存在)
|
||||
if (!source.exists() || target == null) {
|
||||
LogUtils.e(TAG, "【文件管理】文件复制失败:源文件无效或目标文件为空");
|
||||
return false;
|
||||
}
|
||||
// 确保目标目录存在
|
||||
File targetDir = target.getParentFile();
|
||||
if (!targetDir.exists()) {
|
||||
createDirWithPermission(targetDir, "文件复制目标目录");
|
||||
}
|
||||
// 调用FileUtils复制(若项目中已有该方法,可直接复用)
|
||||
return FileUtils.copyFile(source, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:清理裁剪相关临时文件(对外提供,Activity退出时调用)
|
||||
*/
|
||||
public void clearCropTempFiles() {
|
||||
clearOldFile(cropTempFile, "裁剪临时文件");
|
||||
clearOldFile(cropResultFile, "裁剪结果文件");
|
||||
// 清理裁剪目录下的其他临时文件
|
||||
if (fCropTempDir.exists()) {
|
||||
File[] files = fCropTempDir.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
if (file.isFile()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外接口:清理指定旧文件(适配BackgroundSettingsActivity调用)
|
||||
* @param file 要清理的文件
|
||||
* @param fileDesc 文件描述(用于日志打印)
|
||||
*/
|
||||
public void clearOldFileByExternal(File file, String fileDesc) {
|
||||
clearOldFile(file, fileDesc); // 调用内部private方法,复用逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:获取目录类型描述(用于日志调试,明确目录类型)
|
||||
* @param dir 目标目录
|
||||
* @return 目录类型描述
|
||||
*/
|
||||
public String getDirTypeDesc(File dir) {
|
||||
if (dir == null) {
|
||||
return "未知目录(null)";
|
||||
}
|
||||
String dirPath = dir.getAbsolutePath();
|
||||
String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : "";
|
||||
String cachePath = mContext.getCacheDir().getAbsolutePath();
|
||||
|
||||
if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) {
|
||||
return "应用私有外部目录(getExternalFilesDir(),系统裁剪可读写)";
|
||||
} else if (dirPath.contains(cachePath)) {
|
||||
return "应用内部缓存目录(getCacheDir(),兜底目录)";
|
||||
} else {
|
||||
return "外部存储目录(非应用私有,权限受限)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user