From 4e7b7daa427f127859a1467af2a68f526d4bcef3 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Mon, 1 Dec 2025 08:59:33 +0800 Subject: [PATCH] 20251201_085930_533 --- powerbell/build.properties | 4 +- .../BackgroundSettingsActivity.java | 181 ++++++++++++---- .../utils/BackgroundSourceUtils.java | 204 +++++++++++++----- powerbell/src/main/res/xml/file_provider.xml | 5 + 4 files changed, 296 insertions(+), 98 deletions(-) diff --git a/powerbell/build.properties b/powerbell/build.properties index 623ebe73..b707cf25 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -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 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java index f63f4221..7471677e 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -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(精简版,无文件操作) */ diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java index ea5ec303..08893170 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -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 @@ -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 { } } - diff --git a/powerbell/src/main/res/xml/file_provider.xml b/powerbell/src/main/res/xml/file_provider.xml index f0547e40..4ab353de 100644 --- a/powerbell/src/main/res/xml/file_provider.xml +++ b/powerbell/src/main/res/xml/file_provider.xml @@ -24,6 +24,11 @@ name="background_source" path="BackgroundPictureUtils/BackgroundSource/" /> + + +