20251201_085930_533
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(精简版,无文件操作)
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
name="background_source"
|
||||
path="BackgroundPictureUtils/BackgroundSource/" />
|
||||
|
||||
<!-- 应用私有外部存储目录(适配BackgroundSourceUtils的目录) -->
|
||||
<external-files-path
|
||||
name="external_file_path"
|
||||
path="BackgroundSourceUtils/" /> <!-- 对应fUtilsDir(BackgroundSourceUtils根目录) -->
|
||||
|
||||
<!-- 应用外部缓存目录(适配Android11+ 外部缓存场景,如第三方SDK依赖)
|
||||
路径:/storage/emulated/0/Android/data/${applicationId}/cache/
|
||||
关联代码:getExternalCacheDir() -->
|
||||
|
||||
Reference in New Issue
Block a user