20251201_085930_533

This commit is contained in:
2025-12-01 08:59:33 +08:00
parent f7ef8f6b19
commit 4e7b7daa42
4 changed files with 296 additions and 98 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Dec 01 00:19:18 GMT 2025
#Mon Dec 01 00:57:13 GMT 2025
stageCount=13
libraryProject=
baseVersion=15.11
publishVersion=15.11.12
buildCount=36
buildCount=41
baseBetaVersion=15.11.13

View File

@@ -47,6 +47,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.io.InputStream;
public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
@@ -672,41 +673,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
}
}
/**
* 处理选图回调(调用工具类同步预览,精简文件操作)
*/
private void handleSelectPictureResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK || data == null) {
handleOperationCancelOrFail();
return;
}
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("选择的图片Uri为空");
mBgSourceUtils.clearCropTempFiles();
return;
}
LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString());
// 授予持久化权限
grantPersistableUriPermission(selectedImage);
// 解析Uri为文件调用工具类复制文件兜底流复制
File selectedFile = parseUriToFile(selectedImage);
if (selectedFile == null || !selectedFile.exists()) {
ToastUtils.show("选择的图片文件无效");
mBgSourceUtils.clearCropTempFiles();
return;
}
// 同步到预览并启动裁剪(调用工具类保存预览)
mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString());
bvPreviewBackground.reloadPreviewBackground();
startCropImageActivity(false);
LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪");
}
/**
* 处理拍照回调(调用工具类同步预览,精简文件操作)
*/
@@ -804,48 +770,173 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
}
/**
* 辅助函数解析选图Uri为File调用工具类复制,精简逻辑
* 辅助函数解析选图Uri为File核心修复适配Android11+ 共享存储私有路径
* 放弃直接路径读取改用ContentResolver流复制避免Permission denied
*/
/**
* 辅助函数解析选图Uri为File核心修复Android14+ 共享存储私有路径适配)
* 放弃直接路径读取改用ContentResolver流复制避免Permission denied
*/
private File parseUriToFile(Uri uri) {
File targetFile = null;
// 1. 尝试解析路径(兼容旧版本/普通路径)
String filePath = UriUtil.getFilePathFromUri(this, uri);
LogUtils.d(TAG, "【选图解析】Uri解析路径" + filePath);
// 直接解析成功
// 2. 路径有效且可读取兼容Android 10- 或 非隐藏路径)
if (!TextUtils.isEmpty(filePath)) {
targetFile = new File(filePath);
File tempFile = new File(filePath);
// 双重校验:文件存在 + 实际可读取避免canRead()假阳性)
if (isFileActuallyReadable(tempFile)) {
targetFile = tempFile;
} else {
// 路径存在但无权限 → 流复制兜底(核心修复)
targetFile = createTempFileByStreamCopy(uri);
}
} else {
// 解析失败:流复制兜底(调用工具类目录创建)
// 3. 路径解析失败ContentProvider Uri→ 流复制兜底
targetFile = createTempFileByStreamCopy(uri);
}
// 4. 校验目标文件有效性(避免后续逻辑崩溃)
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
LogUtils.e(TAG, "【选图解析】生成的目标文件无效:" + (targetFile != null ? targetFile.getAbsolutePath() : "null"));
ToastUtils.show("图片读取失败,请重新选择");
return null;
}
LogUtils.d(TAG, "【选图解析】Uri解析成功目标文件" + targetFile.getAbsolutePath() + ",大小:" + targetFile.length() + "bytes");
return targetFile;
}
/**
* 辅助函数:通过流复制生成选图临时文件(调用工具类管理目录
* 辅助函数:通过ContentResolver流复制生成临时文件(核心修复:绕开共享存储权限限制
* 直接读取Uri流不依赖文件路径适配所有相册Uri包括私有隐藏路径
*/
private File createTempFileByStreamCopy(Uri uri) {
// 从工具类获取背景源目录,作为选图临时目录(统一路径)
// 1. 初始化临时目录(复用工具类目录,统一路径管理
File tempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp");
if (!tempDir.exists()) {
mBgSourceUtils.copyFile(new File(""), tempDir); // 复用工具类目录创建逻辑
}
File tempFile = new File(tempDir, "selected_temp.jpg");
// 2. 生成唯一临时文件名(避免重复)
String uniqueFileName = "selected_temp_" + System.currentTimeMillis() + ".jpg";
File tempFile = new File(tempDir, uniqueFileName);
// 3. 流复制核心用ContentResolver打开Uri流绕开路径权限限制
InputStream is = null;
FileOutputStream fos = null;
try {
// 清理旧文件
if (tempFile.exists()) {
mBgSourceUtils.clearOldFileByExternal(tempFile, "旧选图临时文件");
}
// 流复制适配ContentProvider Uri
FileUtils.copyStreamToFile(getContentResolver().openInputStream(uri), tempFile);
LogUtils.d(TAG, "【选图解析】流复制生成临时文件:" + tempFile.getAbsolutePath());
// 打开Uri输入流关键Android14+ 仅允许通过ContentResolver读取共享存储私有文件
is = getContentResolver().openInputStream(uri);
if (is == null) {
LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败" + uri.toString());
return null;
}
// 打开目标文件输出流
fos = new FileOutputStream(tempFile);
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区提升复制效率
int len;
// 循环读取流并写入目标文件
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
fos.getFD().sync(); // 确保数据写入磁盘(避免缓冲导致文件损坏)
LogUtils.d(TAG, "【选图解析】流复制成功:" + tempFile.getAbsolutePath() + ",大小:" + tempFile.length() + "bytes");
} catch (Exception e) {
LogUtils.e(TAG, "【选图解析】流复制失败:" + e.getMessage(), e);
tempFile = null;
ToastUtils.show("图片读取失败,请重新选择");
} finally {
// 关闭流资源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());
}
}
}
return tempFile;
}
/**
* 辅助函数校验文件是否实际可读取解决Android14+ canRead()假阳性问题)
*/
private boolean isFileActuallyReadable(File file) {
if (file == null || !file.exists() || !file.isFile()) {
return false;
}
// 实际尝试读取文件(避免路径存在但无权限)
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
fis.read(new byte[1]); // 读取1字节验证权限
return true;
} catch (Exception e) {
LogUtils.w(TAG, "【选图解析】文件存在但无读取权限:" + file.getAbsolutePath());
return false;
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
LogUtils.e(TAG, "【选图解析】校验流关闭失败:" + e.getMessage());
}
}
}
}
/**
* 处理选图回调(新增:目标文件有效性校验,避免后续逻辑崩溃)
*/
private void handleSelectPictureResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK || data == null) {
handleOperationCancelOrFail();
return;
}
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("选择的图片Uri为空");
mBgSourceUtils.clearCropTempFiles();
return;
}
LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString());
// 授予持久化权限Android4.4+
grantPersistableUriPermission(selectedImage);
// 解析Uri为文件核心使用修复后的流复制方法
File selectedFile = parseUriToFile(selectedImage);
if (selectedFile == null || !selectedFile.exists() || selectedFile.length() <= 0) {
ToastUtils.show("选择的图片文件无效");
mBgSourceUtils.clearCropTempFiles();
return;
}
// 同步到预览并启动裁剪(此时文件已存在,避免传入目录路径)
mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString());
bvPreviewBackground.reloadPreviewBackground();
startCropImageActivity(false);
LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪");
}
/**
* 辅助函数从拍照数据中获取Bitmap精简版无文件操作
*/

View File

@@ -8,7 +8,11 @@ import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -21,12 +25,13 @@ import java.io.IOException;
*/
public class BackgroundSourceUtils {
public static final String TAG = "BackgroundPictureUtils";
public static final String TAG = "BackgroundSourceUtils";
// 裁剪相关常量(统一定义,避免硬编码)
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 CROP_INNER_DIR_NAME = "CropInner"; // 优先裁剪目录BackgroundSource下
private static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容
// 1. 静态实例加volatile禁止指令重排保证可见性双重校验锁单例核心
@@ -38,12 +43,14 @@ public class BackgroundSourceUtils {
private BackgroundBean previewBackgroundBean;
// 2. 统一文件目录全量文件管理替代Activity中的目录变量
private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundPictureUtils
private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils
private File fModelDir; // 模型文件目录存储BackgroundBean的JSON文件
private File fBackgroundSourceDir; // 背景图片源目录(存储正式/预览图片)
private File fCropTempDir; // 裁剪临时目录FileProvider适配路径系统裁剪应用可读写
private File fCropFallbackDir; // 裁剪兜底目录(应用私有外部目录失败时使用)
private File fCropInnerDir; // 新增优先裁剪目录BackgroundSource下权限更可控
private File cropTempFile; // 裁剪临时文件(系统裁剪应用写入目标)
private File cropInnerTempFile; // 新增优先裁剪临时文件CropInner目录下
private File cropResultFile; // 裁剪结果文件(裁剪后保存的最终文件)
// 3. 私有构造器(加防反射逻辑+初始化所有目录/文件)
@@ -62,7 +69,7 @@ public class BackgroundSourceUtils {
loadSettings();
}
// 4. 双重校验锁单例(线程安全,高效,支持多线程并发调用)
// 4. 双重校验锁单例(线程安全,高效,支持多线程并发调用Java7语法兼容
public static BackgroundSourceUtils getInstance(Context context) {
// 第一重校验:避免每次调用都加锁(提升效率)
if (sInstance == null) {
@@ -78,26 +85,28 @@ public class BackgroundSourceUtils {
}
/**
* 初始化所有文件目录(统一管理替代Activity中的目录初始化逻辑
* 包含:工具类根目录、模型目录、背景图目录、裁剪临时目录、裁剪兜底目录
* 初始化所有文件目录(修改优先初始化CropInner目录确保权限可控
* 包含:工具类根目录、模型目录、背景图目录、裁剪临时目录、裁剪兜底目录、优先裁剪目录
*/
private void initAllDirs() {
// 1. 工具类根目录(应用外部存储:/Android/data/包名/files/BackgroundPictureUtils
// 1. 工具类根目录(应用外部存储:/Android/data/包名/files/BackgroundSourceUtils
fUtilsDir = mContext.getExternalFilesDir(TAG);
if (fUtilsDir == null) {
LogUtils.e(TAG, "【文件管理】应用外部存储不可用,切换到应用内部缓存目录");
fUtilsDir = mContext.getCacheDir(); // 极端兜底:应用内部缓存目录
}
// 2. 子目录初始化(按功能划分,确保目录存在并设置权限
// 2. 子目录初始化(按功能划分,新增优先裁剪目录CropInner
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); // 裁剪兜底目录
fCropInnerDir = new File(fBackgroundSourceDir, CROP_INNER_DIR_NAME); // 优先裁剪目录BackgroundSource下
// 3. 递归创建所有目录(确保目录存在Android14+ 必需
// 3. 递归创建所有目录(修改优先创建CropInner目录确保权限初始化
createDirWithPermission(fModelDir, "模型文件目录");
createDirWithPermission(fBackgroundSourceDir, "背景图片目录");
createDirWithPermission(fCropInnerDir, "优先裁剪目录BackgroundSource下"); // 优先创建
createDirWithPermission(fCropTempDir, "裁剪临时目录FileProvider适配");
createDirWithPermission(fCropFallbackDir, "裁剪兜底目录");
@@ -105,42 +114,61 @@ public class BackgroundSourceUtils {
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
LogUtils.d(TAG, "【文件管理】所有目录初始化完成:根目录=" + fUtilsDir.getAbsolutePath());
LogUtils.d(TAG, "【文件管理】所有目录初始化完成:根目录=" + fUtilsDir.getAbsolutePath() + ",优先裁剪目录=" + fCropInnerDir.getAbsolutePath());
}
/**
* 初始化所有文件(统一管理替代Activity中的文件初始化逻辑
* 包含:裁剪临时文件、裁剪结果文件
* 初始化所有文件(修改:新增优先裁剪文件初始化)
* 包含:优先裁剪临时文件、原裁剪临时文件、裁剪结果文件
*/
private void initAllFiles() {
// 1. 裁剪临时文件(系统裁剪应用写入路径,优先裁剪临时目录
// 1. 新增:优先裁剪临时文件(BackgroundSource/CropInner下FileProvider已配置
cropInnerTempFile = new File(fCropInnerDir, CROP_TEMP_FILE_NAME);
// 2. 原裁剪临时文件(兼容旧逻辑)
cropTempFile = new File(fCropTempDir, CROP_TEMP_FILE_NAME);
// 2. 裁剪结果文件(裁剪后保存的最终文件,存入背景图片目录)
// 3. 裁剪结果文件(裁剪后保存的最终文件,存入背景图片目录)
cropResultFile = new File(fBackgroundSourceDir, CROP_RESULT_FILE_NAME);
// 3. 初始化时清理旧文件(避免文件锁定/权限残留)
// 4. 初始化时清理旧文件(避免文件锁定/权限残留)
clearOldFile(cropInnerTempFile, "旧优先裁剪临时文件"); // 清理优先裁剪文件
clearOldFile(cropTempFile, "旧裁剪临时文件");
clearOldFile(cropResultFile, "旧裁剪结果文件");
LogUtils.d(TAG, "【文件管理】所有文件初始化完成:裁剪临时文件=" + cropTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath());
LogUtils.d(TAG, "【文件管理】所有文件初始化完成:优先裁剪临时文件=" + cropInnerTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath());
}
/**
* 核心函数为系统裁剪应用创建可读写的FileProvider路径满足需求
* 功能确保裁剪临时目录fCropTempDir可读写返回裁剪临时文件系统裁剪应用写入目标
* 适配Android14+、MIUI解决Permission denied问题确保系统裁剪应用能正常写入
* 核心函数为系统裁剪应用创建可读写的FileProvider路径修改优先使用CropInner目录
* 适配Android14+、MIUI解决Permission denied+裁剪文件为0字节问题
* @return 裁剪临时文件File系统裁剪应用可读写路径已适配FileProvider
*/
public File createCropFileProviderPath() {
LogUtils.d(TAG, "【裁剪路径】createCropFileProviderPath 触发,创建系统裁剪可读写路径");
// 1. 优先尝试裁剪临时目录(FileProvider适配路径
// 1. 优先使用BackgroundSource下的CropInner目录核心修改权限可控FileProvider已配置
if (fCropInnerDir != null && fCropInnerDir.exists() && isDirActuallyWritable(fCropInnerDir)) {
try {
// 重新初始化优先裁剪临时文件(先删后建,避免文件锁定)
clearOldFile(cropInnerTempFile, "优先裁剪临时文件(重新初始化)");
cropInnerTempFile.createNewFile();
// 强制设置文件权限(系统裁剪应用必需:允许所有用户读写)
setFilePermissions(cropInnerTempFile);
// 关键将当前裁剪文件指向优先裁剪文件上层Activity直接使用
cropTempFile = cropInnerTempFile;
LogUtils.d(TAG, "【裁剪路径】系统裁剪可读写路径创建成功(优先裁剪目录):" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】优先裁剪目录创建文件失败:" + e.getMessage(), e);
}
} else {
LogUtils.w(TAG, "【裁剪路径】优先裁剪目录不可用(不存在或无权限),切换到备用目录");
}
// 2. 备用尝试裁剪临时目录FileProvider适配路径
if (isDirActuallyWritable(fCropTempDir)) {
try {
// 重新初始化裁剪临时文件(先删后建,避免文件锁定)
clearOldFile(cropTempFile, "裁剪临时文件(重新初始化)");
cropTempFile.createNewFile();
// 强制设置文件权限(系统裁剪应用必需:允许所有用户读写)
setFilePermissions(cropTempFile);
LogUtils.d(TAG, "【裁剪路径】系统裁剪可读写路径创建成功(裁剪临时目录):" + cropTempFile.getAbsolutePath());
return cropTempFile;
@@ -149,7 +177,7 @@ public class BackgroundSourceUtils {
}
}
// 2. 兜底1裁剪临时目录失败,切换到裁剪兜底目录
// 3. 兜底1裁剪兜底目录
if (isDirActuallyWritable(fCropFallbackDir)) {
try {
cropTempFile = new File(fCropFallbackDir, CROP_TEMP_FILE_NAME);
@@ -163,7 +191,7 @@ public class BackgroundSourceUtils {
}
}
// 3. 终极兜底:应用内部缓存目录(确保不崩溃
// 4. 终极兜底:应用内部缓存目录(提示用户权限问题
File cacheDir = mContext.getCacheDir();
if (isDirActuallyWritable(cacheDir)) {
try {
@@ -171,11 +199,12 @@ public class BackgroundSourceUtils {
clearOldFile(cropTempFile, "裁剪临时文件(终极兜底)");
cropTempFile.createNewFile();
setFilePermissions(cropTempFile);
LogUtils.w(TAG, "【裁剪路径】应用外部目录全部失败,终极兜底到缓存目录:" + cropTempFile.getAbsolutePath());
LogUtils.w(TAG, "【裁剪路径】应用外部目录全部失败,终极兜底到缓存目录MIUI可能裁剪失败" + cropTempFile.getAbsolutePath());
ToastUtils.show("存储权限受限,建议授予「所有文件访问权限」以确保裁剪正常");
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】终极兜底目录创建文件失败:" + e.getMessage(), e);
ToastUtils.show("裁剪路径创建失败,请重启应用");
ToastUtils.show("裁剪路径创建失败,请重启应用并授予存储权限");
}
}
@@ -288,15 +317,79 @@ public class BackgroundSourceUtils {
}
/**
* 保存图片到预览Bean核心修复解决文件名覆盖+路径无效问题
* 新增流复制文件核心修复Android14+ 共享存储权限限制适配
* 不依赖文件路径直接通过流复制支持读取相册私有隐藏文件避免Permission denied
* @param source 源文件(可为共享存储私有文件)
* @param target 目标文件(应用私有目录,确保可写)
* @return true=复制成功false=失败
*/
public boolean copyFileByStream(File source, File target) {
if (source == null || !source.exists() || !source.isFile() || target == null) {
LogUtils.e(TAG, "【文件管理】流复制失败:源文件无效或目标文件为空");
return false;
}
// 确保目标目录存在
File targetDir = target.getParentFile();
if (!targetDir.exists()) {
createDirWithPermission(targetDir, "流复制目标目录");
}
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 打开源文件输入流(支持共享存储私有文件)
fis = new FileInputStream(source);
// 打开目标文件输出流
fos = new FileOutputStream(target);
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区提升复制效率
int len;
// 循环读取流并写入目标文件Java7 普通for循环兼容语法
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
fos.getFD().sync(); // 强制同步到磁盘确保文件写入完成Java7 支持)
LogUtils.d(TAG, "【文件管理】流复制成功:" + source.getAbsolutePath() + "" + target.getAbsolutePath() + ",大小:" + target.length() + "bytes");
return true;
} catch (Exception e) {
LogUtils.e(TAG, "【文件管理】流复制异常:" + e.getMessage(), e);
// 复制失败时删除目标文件(避免残留空文件)
if (target.exists()) {
clearOldFileByExternal(target, "流复制失败残留文件");
}
return false;
} finally {
// 关闭流资源Java7 手动关闭避免内存泄漏不使用try-with-resources
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
LogUtils.e(TAG, "【文件管理】源文件流关闭失败:" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "【文件管理】目标文件流关闭失败:" + e.getMessage());
}
}
}
}
/**
* 保存图片到预览Bean核心修复替换路径复制为流复制避免预览路径错误
* @param sourceFile 源图片文件(非空,必须存在)
* @param fileInfo 图片附加信息如Uri字符串仅作备注
* @return 更新后的预览Bean
*/
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
// 校验源文件合法性
if (sourceFile == null || !sourceFile.exists() || !sourceFile.isFile()) {
LogUtils.e(TAG, "【文件管理】源文件无效:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
// 强化校验源文件必须存在、是文件、大小>0
if (sourceFile == null || !sourceFile.exists() || !sourceFile.isFile() || sourceFile.length() <= 0) {
LogUtils.e(TAG, "【文件管理】源文件无效:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null") + ",大小:" + (sourceFile != null ? sourceFile.length() : 0) + "bytes");
ToastUtils.show("源图片文件无效");
return previewBackgroundBean;
}
@@ -310,26 +403,26 @@ public class BackgroundSourceUtils {
String uniqueFileName = FileUtils.createUniqueFileName(sourceFile);
File previewBackgroundFile = new File(fBackgroundSourceDir, uniqueFileName);
// 复制源文件到预览目录(确保图片实际保存成功)
boolean copySuccess = FileUtils.copyFile(sourceFile, previewBackgroundFile);
// 核心修改用流复制替代原FileUtils.copyFile解决共享存储权限问题
boolean copySuccess = copyFileByStream(sourceFile, previewBackgroundFile);
if (!copySuccess) {
LogUtils.e(TAG, "【文件管理】图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + "" + previewBackgroundFile.getAbsolutePath());
ToastUtils.show("预览图片保存失败");
return previewBackgroundBean;
}
// 正确赋值预览Bean核心修复不覆盖文件名将附加信息存入backgroundFileInfo
// 正确赋值预览Bean确保文件名非空,避免后续路径为空
previewBackgroundBean = new BackgroundBean();
previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 正确赋值:唯一文件名
previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 唯一文件名(非空)
previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_" + previewBackgroundFile.getName()); // 压缩文件名(前缀标识)
previewBackgroundBean.setBackgroundFileInfo(fileInfo); // 正确赋值:附加信息Uri
previewBackgroundBean.setBackgroundFileInfo(fileInfo); // 附加信息Uri
previewBackgroundBean.setIsUseBackgroundFile(true); // 标记使用背景图
previewBackgroundBean.setIsUseScaledCompress(true); // 启用压缩图,提升预览加载速度
previewBackgroundBean.setBackgroundWidth(100); // 默认宽高比1:1(可根据需求调整)
previewBackgroundBean.setIsUseScaledCompress(true); // 启用压缩图
previewBackgroundBean.setBackgroundWidth(100); // 默认宽高比1:1
previewBackgroundBean.setBackgroundHeight(100);
saveSettings(); // 持久化保存预览Bean
LogUtils.d(TAG, "【文件管理】预览图片保存成功:" + previewBackgroundFile.getAbsolutePath());
LogUtils.d(TAG, "【文件管理】预览图片保存成功:" + previewBackgroundFile.getAbsolutePath() + ",大小:" + previewBackgroundFile.length() + "bytes");
ToastUtils.show("预览图片加载成功");
return previewBackgroundBean;
}
@@ -417,10 +510,11 @@ public class BackgroundSourceUtils {
LogUtils.d(TAG, "【权限管理】目录权限设置完成:路径=" + dir.getAbsolutePath() + ",可写=" + dir.canWrite() + ",可读=" + dir.canRead());
// 递归处理子目录和文件
// 递归处理子目录和文件Java7 普通for循环兼容语法
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
setDirPermissionsRecursively(file);
} else {
@@ -464,16 +558,11 @@ public class BackgroundSourceUtils {
}
/**
* 工具方法:清理旧文件(避免文件锁定/残留)
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
/**
* 工具方法:清理旧文件(避免文件锁定/残留)【内部私有,不对外暴露】
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
private void clearOldFile(File file, String fileDesc) {
private void clearOldFile(File file, String fileDesc) {
if (file == null) {
return;
}
@@ -504,11 +593,12 @@ public class BackgroundSourceUtils {
boolean createSuccess = testFile.createNewFile();
if (createSuccess) {
boolean canWrite = testFile.canWrite();
boolean canRead = testFile.canRead();
testFile.delete(); // 删除临时文件,不占用空间
LogUtils.d(TAG, "【权限校验】目录实际写入校验:" + dir.getAbsolutePath() + ",结果=" + (canWrite ? "通过" : "失败"));
LogUtils.d(TAG, "【权限校验】目录实际写入校验:" + dir.getAbsolutePath() + "创建成功=" + createSuccess + ",可写=" + canWrite + ",可读=" + canRead + "结果=" + (canWrite ? "通过" : "失败"));
return canWrite;
} else {
LogUtils.d(TAG, "【权限校验】目录实际写入校验失败:" + dir.getAbsolutePath() + ",创建临时文件失败");
LogUtils.d(TAG, "【权限校验】目录实际写入校验失败:" + dir.getAbsolutePath() + ",创建临时文件失败Permission denied");
return false;
}
} catch (IOException e) {
@@ -558,11 +648,24 @@ public class BackgroundSourceUtils {
public void clearCropTempFiles() {
clearOldFile(cropTempFile, "裁剪临时文件");
clearOldFile(cropResultFile, "裁剪结果文件");
// 清理裁剪目录下的其他临时文件
// 清理裁剪目录下的其他临时文件Java7 普通for循环
if (fCropTempDir.exists()) {
File[] files = fCropTempDir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isFile()) {
file.delete();
}
}
}
}
// 新增清理优先裁剪目录CropInner下的临时文件
if (fCropInnerDir != null && fCropInnerDir.exists()) {
File[] files = fCropInnerDir.listFiles();
if (files != null && files.length > 0) {
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isFile()) {
file.delete();
}
@@ -572,7 +675,7 @@ public class BackgroundSourceUtils {
LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成");
}
/**
/**
* 对外接口清理指定旧文件适配BackgroundSettingsActivity调用
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
@@ -604,4 +707,3 @@ public class BackgroundSourceUtils {
}
}

View File

@@ -24,6 +24,11 @@
name="background_source"
path="BackgroundPictureUtils/BackgroundSource/" />
<!-- 应用私有外部存储目录适配BackgroundSourceUtils的目录 -->
<external-files-path
name="external_file_path"
path="BackgroundSourceUtils/" /> <!-- 对应fUtilsDirBackgroundSourceUtils根目录 -->
<!-- 应用外部缓存目录适配Android11+ 外部缓存场景如第三方SDK依赖
路径:/storage/emulated/0/Android/data/${applicationId}/cache/
关联代码getExternalCacheDir() -->