图片剪裁调试完成

This commit is contained in:
2025-12-01 09:57:49 +08:00
parent 4e7b7daa42
commit e14744b2ac
4 changed files with 422 additions and 373 deletions

View File

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

View File

@@ -154,7 +154,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败");
}
}
/**
* 处理分享图片意图仅UI逻辑文件处理调用工具类
*/
@@ -282,37 +282,71 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
};
// 点击事件:固定比例裁剪(调用工具类获取裁剪路径,无文件逻辑)
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能");
// 从工具类获取正式背景文件,校验有效性
File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath());
if (targetFile.exists()) {
startCropImageActivity(false);
LogUtils.d(TAG, "【裁剪启动】固定比例裁剪已启动");
} else {
ToastUtils.show("无可用裁剪图片,请先选择/拍照");
LogUtils.e(TAG, "裁剪失败】无可用裁剪图片");
}
}
};
// 点击事件固定比例裁剪添加MIUI裁剪提示
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能");
// 从工具类获取正式背景文件,校验有效性
File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath());
if (targetFile.exists()) {
// 适配MIUI弹出裁剪提示建议选择系统相机
if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) {
new AlertDialog.Builder(BackgroundSettingsActivity.this)
.setTitle("裁剪提示")
.setMessage("若裁剪失败,请选择「系统相机」作为裁剪工具")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startCropImageActivity(false); // 启动固定比例裁剪
}
})
.setNegativeButton("取消", null)
.show();
} else {
// 非MIUI机型直接启动裁剪
startCropImageActivity(false);
}
LogUtils.d(TAG, "【裁剪启动】固定比例裁剪已启动");
} else {
ToastUtils.show("无可用裁剪图片,请先选择/拍照");
LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片");
}
}
};
// 点击事件:自由裁剪(调用工具类获取裁剪路径,无文件逻辑)
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】触发自由裁剪功能");
File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath());
if (targetFile.exists()) {
startCropImageActivity(true);
LogUtils.d(TAG, "【裁剪启动】自由裁剪已启动");
} else {
ToastUtils.show("无可用裁剪图片,请先选择/拍照");
LogUtils.e(TAG, "裁剪失败】无可用裁剪图片");
}
}
};
// 点击事件自由裁剪添加MIUI裁剪提示
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】触发自由裁剪功能");
File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath());
if (targetFile.exists()) {
// 适配MIUI弹出裁剪提示建议选择系统相机
if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) {
new AlertDialog.Builder(BackgroundSettingsActivity.this)
.setTitle("裁剪提示")
.setMessage("若裁剪失败,请选择「系统相机」作为裁剪工具")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startCropImageActivity(true); // 启动自由裁剪
}
})
.setNegativeButton("取消", null)
.show();
} else {
// 非MIUI机型直接启动裁剪
startCropImageActivity(true);
}
LogUtils.d(TAG, "【裁剪启动】自由裁剪已启动");
} else {
ToastUtils.show("无可用裁剪图片,请先选择/拍照");
LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片");
}
}
};
// 点击事件:拍照(仅权限+相机意图,文件处理调用工具类)
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
@@ -492,118 +526,131 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
* @param isCropFree 是否自由裁剪
*/
public void startCropImageActivity(boolean isCropFree) {
LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree);
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
previewBean.setIsUseScaledCompress(true);
mBgSourceUtils.saveSettings();
LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree);
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
previewBean.setIsUseScaledCompress(true);
mBgSourceUtils.saveSettings();
// 1. 校验预览图片有效性(从工具类获取路径
File previewFile = new File(mBgSourceUtils.getPreviewBackgroundFilePath());
LogUtils.d(TAG, "【裁剪校验】预览图片状态:路径=" + previewFile.getAbsolutePath() + ",是否存在=" + previewFile.exists());
if (!previewFile.exists() || previewFile.length() <= 0) {
ToastUtils.show("预览图片不存在或损坏");
LogUtils.e(TAG, "【裁剪失败】预览图片无效");
return;
}
// 1. 预览图片有效性校验(保留强化校验逻辑
String previewFilePath = mBgSourceUtils.getPreviewBackgroundFilePath();
if (TextUtils.isEmpty(previewFilePath)) {
ToastUtils.show("预览图片路径为空");
LogUtils.e(TAG, "【裁剪失败】预览图片路径为空");
return;
}
File previewFile = new File(previewFilePath);
LogUtils.d(TAG, "【裁剪校验】预览图片状态:路径=" + previewFile.getAbsolutePath() + ",是否存在=" + previewFile.exists() + ",是否为文件=" + previewFile.isFile() + ",大小=" + (previewFile.exists() ? previewFile.length() : 0) + "bytes");
if (!previewFile.exists() || !previewFile.isFile() || previewFile.length() <= 100) {
ToastUtils.show("预览图片不存在或损坏");
LogUtils.e(TAG, "【裁剪失败】预览图片无效");
return;
}
// 2. 生成裁剪输入Uri调用工具类的FileProvider Authority
Uri inputUri = null;
try {
inputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), previewFile);
LogUtils.d(TAG, "【裁剪Uri】输入Uri生成成功 : " + inputUri.toString());
} catch (Exception e) {
LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败" + e.getMessage(), e);
ToastUtils.show("图片裁剪失败:无法获取图片权限");
mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理临时文件
return;
}
// 2. 生成裁剪输入Uri强化MIUI权限授予
Uri inputUri = null;
try {
inputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), previewFile);
// 显式授予MIUI裁剪工具读写权限关键适配
grantUriPermission("com.miui.gallery", inputUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
LogUtils.d(TAG, "【裁剪Uri】输入Uri生成成功 : " + inputUri.toString() + "已授予MIUI裁剪工具权限");
} catch (Exception e) {
LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败" + e.getMessage(), e);
ToastUtils.show("图片裁剪失败:无法获取图片权限");
mBgSourceUtils.clearCropTempFiles();
return;
}
// 3. 调用工具类创建裁剪路径(系统裁剪可读写
File cropTempFile = mBgSourceUtils.createCropFileProviderPath();
if (cropTempFile == null) {
ToastUtils.show("裁剪路径创建失败,请重试");
return;
}
// 3. 调用工具类创建裁剪路径(原有逻辑不变
File cropTempFile = mBgSourceUtils.createCropFileProviderPath();
if (cropTempFile == null) {
ToastUtils.show("裁剪路径创建失败,请重试");
return;
}
// 4. 构建裁剪意图(精简参数,保留核心配置
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(inputUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("noFaceDetection", true);
// 4. 构建裁剪意图(适配MIUI核心修改
Intent intent = new Intent("com.android.camera.action.CROP");
// 恢复inputUri和类型移除setDataAndType(null, null)MIUI必需
intent.setDataAndType(inputUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("noFaceDetection", true);
// 设置裁剪比例
if (!isCropFree) {
int viewWidth = bvPreviewBackground.getWidth() > 0 ? bvPreviewBackground.getWidth() : getResources().getDisplayMetrics().widthPixels;
int viewHeight = bvPreviewBackground.getHeight() > 0 ? bvPreviewBackground.getHeight() : getResources().getDisplayMetrics().heightPixels;
int gcd = calculateGCD(viewWidth, viewHeight);
intent.putExtra("aspectX", viewWidth / gcd);
intent.putExtra("aspectY", viewHeight / gcd);
} else {
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
}
// 裁剪比例设置(原有逻辑不变)
if (!isCropFree) {
int viewWidth = bvPreviewBackground.getWidth() > 0 ? bvPreviewBackground.getWidth() : getResources().getDisplayMetrics().widthPixels;
int viewHeight = bvPreviewBackground.getHeight() > 0 ? bvPreviewBackground.getHeight() : getResources().getDisplayMetrics().heightPixels;
int gcd = calculateGCD(viewWidth, viewHeight);
intent.putExtra("aspectX", viewWidth / gcd);
intent.putExtra("aspectY", viewHeight / gcd);
} else {
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
}
// 设置输出尺寸适配Android14+
int maxOutputSize = Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU ? 1536 : 2048;
int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize);
int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize);
intent.putExtra("outputX", outputX);
intent.putExtra("outputY", outputY);
intent.putExtra("scale", true);
intent.putExtra("scaleUpIfNeeded", true);
// 输出尺寸设置(原有逻辑不变
int maxOutputSize = Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU ? 1536 : 2048;
int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize);
int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize);
intent.putExtra("outputX", outputX);
intent.putExtra("outputY", outputY);
intent.putExtra("scale", true);
intent.putExtra("scaleUpIfNeeded", true);
// 输出配置
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("quality", 80);
intent.putExtra("return-data", false); // 禁用返回Bitmap避免OOM
// 输出配置指定JPEG格式适配MIUI
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("quality", 80);
intent.putExtra("return-data", false); // 禁用返回Bitmap避免OOM
// 权限Flags
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) {
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}
// 权限Flags添加持久化权限适配MIUI
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) {
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}
// 5. 适配系统裁剪工具(显式指定Component
try {
List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (!resolveInfos.isEmpty()) {
ResolveInfo resolveInfo = resolveInfos.get(0);
String cropPackageName = resolveInfo.activityInfo.packageName;
String cropActivityName = resolveInfo.activityInfo.name;
LogUtils.d(TAG, "【裁剪适配】找到裁剪工具:包名=" + cropPackageName + "Activity=" + cropActivityName);
// 5. 适配系统裁剪工具(MIUI专属处理
try {
List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (!resolveInfos.isEmpty()) {
ResolveInfo resolveInfo = resolveInfos.get(0);
String cropPackageName = resolveInfo.activityInfo.packageName;
String cropActivityName = resolveInfo.activityInfo.name;
LogUtils.d(TAG, "【裁剪适配】找到裁剪工具:包名=" + cropPackageName + "Activity=" + cropActivityName);
// 生成输出Uri授予裁剪工具权限)
Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile);
// 显式设置Component避免参数冲突
Intent cropIntent = new Intent(intent);
cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName));
cropIntent.setDataAndType(null, null);
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
// 重新添加权限
cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// 生成输出Uri显式授予MIUI写入权限)
Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile);
// 适配MIUI裁剪工具额外授予持久化写入权限
if (cropPackageName.equals("com.miui.gallery")) {
grantUriPermission(cropPackageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
LogUtils.d(TAG, "【裁剪适配】已授予MIUI裁剪工具输出Uri写入权限");
}
startActivityForResult(cropIntent, REQUEST_CROP_IMAGE);
LogUtils.d(TAG, "【裁剪启动】已启动系统裁剪工具,输出路径:" + cropTempFile.getAbsolutePath());
} else {
// 兜底:启动第三方裁剪工具
Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
Intent chooser = Intent.createChooser(intent, "选择裁剪工具");
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (chooser.resolveActivity(getPackageManager()) != null) {
startActivityForResult(chooser, REQUEST_CROP_IMAGE);
} else {
ToastUtils.show("无可用裁剪工具,请安装系统相机");
mBgSourceUtils.clearCropTempFiles();
}
}
} catch (Exception e) {
LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e);
ToastUtils.show("法启动裁剪工具");
mBgSourceUtils.clearCropTempFiles();
}
}
// 显式设置Component移除setDataAndType(null, null),避免参数冲突)
Intent cropIntent = new Intent(intent);
cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName));
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(cropIntent, REQUEST_CROP_IMAGE);
LogUtils.d(TAG, "【裁剪启动】已启动系统裁剪工具,输出路径:" + cropTempFile.getAbsolutePath());
} else {
// 兜底:启动第三方裁剪工具(原有逻辑不变)
Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
Intent chooser = Intent.createChooser(intent, "选择裁剪工具");
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (chooser.resolveActivity(getPackageManager()) != null) {
startActivityForResult(chooser, REQUEST_CROP_IMAGE);
} else {
ToastUtils.show("可用裁剪工具,请安装系统相机");
mBgSourceUtils.clearCropTempFiles();
}
}
} catch (Exception e) {
LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e);
ToastUtils.show("无法启动裁剪工具");
mBgSourceUtils.clearCropTempFiles();
}
}
/**
* 计算最大公约数(简化裁剪比例)
@@ -710,31 +757,39 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
* 处理裁剪回调(调用工具类获取裁剪文件,精简文件操作)
*/
private void handleCropImageResult(int requestCode, int resultCode, Intent data) {
// 从工具类获取裁剪临时文件(唯一裁剪文件入口,无本地文件管理
// 从工具类获取裁剪临时文件(唯一入口
File cropTempFile = mBgSourceUtils.getCropTempFile();
boolean isFileExist = cropTempFile != null && cropTempFile.exists();
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
long fileSize = isFileExist ? cropTempFile.length() : 0;
boolean isFileValid = isFileExist && isFileReadable && fileSize > 100; // 大于100字节视为有效
boolean isCropSuccess = (resultCode == RESULT_OK) || isFileValid;
// 适配MIUI仅resultCode=RESULT_OK且文件有效时视为成功resultCode=0视为取消
boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100;
// 打印校验日志(精简版,保留核心信息)
// 打印校验日志
LogUtils.d(TAG, "【裁剪回调】校验resultCode=" + resultCode + ",文件存在=" + isFileExist + ",大小=" + fileSize + "bytes是否成功=" + isCropSuccess);
// 处理MIUI 0字节文件问题
if (isFileExist && fileSize == 0) {
LogUtils.e(TAG, "【裁剪失败】裁剪文件为空MIUI适配问题");
ToastUtils.show("裁剪失败,请选择系统相机裁剪重试");
mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理无效文件
// 处理MIUI裁剪取消resultCode=0视为取消而非失败
if (resultCode == 0 && !isCropSuccess) {
LogUtils.d(TAG, "【裁剪回调】MIUI 裁剪工具已取消");
ToastUtils.show("裁剪已取消");
mBgSourceUtils.clearCropTempFiles();
return;
}
// 裁剪成功解析Bitmap并保存
// 处理裁剪文件为空(真正的裁剪失败)
if (isFileExist && fileSize == 0) {
LogUtils.e(TAG, "【裁剪失败】裁剪文件为空MIUI适配问题");
ToastUtils.show("裁剪失败,请尝试选择「系统相机」裁剪或更换图片");
mBgSourceUtils.clearCropTempFiles();
return;
}
// 裁剪成功解析Bitmap并保存原有逻辑不变
if (isCropSuccess) {
Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
if (cropBitmap != null && !cropBitmap.isRecycled()) {
saveCropBitmap(cropBitmap); // 保存裁剪结果
doubleRefreshPreview(); // 双重刷新预览
doubleRefreshPreview(); // 双重刷新预览适配MIUI渲染延迟
LogUtils.d(TAG, "【裁剪完成】裁剪回调处理结束");
} else {
ToastUtils.show("获取裁剪图片失败");
@@ -742,7 +797,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
mBgSourceUtils.clearCropTempFiles();
}
} else {
// 裁剪取消/失败:统一处理
// 其他失败场景(统一处理
handleOperationCancelOrFail();
}
}

View File

@@ -17,22 +17,18 @@ import java.io.OutputStream;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 12:07:20
* @Describe 背景图片工具集(全量文件管理+裁剪FileProvider路径适配线程安全+数据流转正常
* 核心能力:
* 1. 统一管理所有文件路径(背景图/裁剪临时文件/压缩图)
* 2. 为系统裁剪应用创建可读写的FileProvider路径适配Android14+ MIUI
* 3. 替代BackgroundSettingsActivity的所有文件操作逻辑
* @Describe 背景图片工具集(调整版:图片存储→/Pictures/PowerBellJSON→应用外置存储
*/
public class BackgroundSourceUtils {
public static final String TAG = "BackgroundSourceUtils";
// 裁剪相关常量(统一定义,避免硬编码)
private static final String CROP_TEMP_DIR_NAME = "CropTemp"; // 裁剪临时目录(FileProvider适配
private static final String CROP_TEMP_DIR_NAME = "cache"; // 裁剪缓存目录(基础目录下
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"; // 多包名兼容
// 图片操作基础目录(核心调整:系统公共图片目录)
private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell";
// 1. 静态实例加volatile禁止指令重排保证可见性双重校验锁单例核心
private static volatile BackgroundSourceUtils sInstance;
@@ -42,16 +38,17 @@ public class BackgroundSourceUtils {
private File previewBackgroundBeanFile;
private BackgroundBean previewBackgroundBean;
// 2. 统一文件目录(全量文件管理替代Activity中的目录变量
// 2. 统一文件目录(分两类图片目录→系统公共目录JSON目录→应用外置存储
// 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/
private File fPictureBaseDir; // 图片基础目录
private File fPictureCacheDir; // 裁剪缓存目录(基础目录下/cache
private File fBackgroundSourceDir; // 图片存储目录(基础目录下,存储正式/预览图)
// JSON配置目录原应用外置存储目录不改变
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; // 裁剪结果文件(裁剪后保存的最终文件)
private File fModelDir; // 模型文件目录存储JSON配置
// 裁剪文件统一放入图片基础目录下的cache
private File cropTempFile; // 裁剪临时文件fPictureCacheDir下
private File cropResultFile; // 裁剪结果文件fBackgroundSourceDir下
// 3. 私有构造器(加防反射逻辑+初始化所有目录/文件)
private BackgroundSourceUtils(Context context) {
@@ -61,21 +58,19 @@ public class BackgroundSourceUtils {
}
// 上下文用Application Context避免Activity内存泄漏
this.mContext = context.getApplicationContext();
// 初始化所有目录(文件管理核心:统一创建+权限设置
initAllDirs();
// 初始化目录(分图片目录+JSON目录
initPictureDirs(); // 初始化图片操作目录(系统公共目录)
initJsonDirs(); // 初始化JSON配置目录应用外置存储
// 初始化所有文件(裁剪临时文件/结果文件等)
initAllFiles();
// 加载配置(正式/预览Bean
// 加载配置(正式/预览BeanJSON目录下
loadSettings();
}
// 4. 双重校验锁单例线程安全高效支持多线程并发调用Java7语法兼容
public static BackgroundSourceUtils getInstance(Context context) {
// 第一重校验:避免每次调用都加锁(提升效率)
if (sInstance == null) {
// 同步锁:保证同一时刻只有一个线程进入创建逻辑
synchronized (BackgroundSourceUtils.class) {
// 第二重校验:防止多线程并发时重复创建(核心)
if (sInstance == null) {
sInstance = new BackgroundSourceUtils(context);
}
@@ -85,113 +80,101 @@ public class BackgroundSourceUtils {
}
/**
* 初始化所有文件目录修改优先初始化CropInner目录确保权限可控
* 包含:工具类根目录、模型目录、背景图目录、裁剪临时目录、裁剪兜底目录、优先裁剪目录
* 初始化图片操作目录(核心调整:系统公共图片目录 /Pictures/PowerBell/
*/
private void initAllDirs() {
// 1. 工具类根目录(应用外部存储:/Android/data/包名/files/BackgroundSourceUtils
private void initPictureDirs() {
// 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell
fPictureBaseDir = new File(PICTURE_BASE_DIR);
// 2. 图片存储目录:基础目录下(存储正式/预览图片)
fBackgroundSourceDir = new File(fPictureBaseDir, "BackgroundSource");
// 3. 裁剪缓存目录:基础目录下/cache所有裁剪操作在此目录
fPictureCacheDir = new File(fPictureBaseDir, CROP_TEMP_DIR_NAME);
// 4. 递归创建目录(系统公共目录需强制授权,确保创建成功)
createDirWithPermission(fPictureBaseDir, "图片基础目录(/Pictures/PowerBell");
createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下)");
createDirWithPermission(fPictureCacheDir, "裁剪缓存目录(基础目录/cache");
LogUtils.d(TAG, "【图片目录初始化】完成:基础目录=" + fPictureBaseDir.getAbsolutePath() + ",裁剪缓存目录=" + fPictureCacheDir.getAbsolutePath());
}
/**
* 初始化JSON配置目录保留原逻辑应用外置存储
*/
private void initJsonDirs() {
// 1. 工具类根目录(应用外置存储)
fUtilsDir = mContext.getExternalFilesDir(TAG);
if (fUtilsDir == null) {
LogUtils.e(TAG, "文件管理】应用外存储不可用,切换到应用内部缓存目录");
fUtilsDir = mContext.getCacheDir(); // 极端兜底:应用内部缓存目录
LogUtils.e(TAG, "JSON目录】应用外存储不可用,切换到应用内部缓存目录");
fUtilsDir = mContext.getCacheDir();
}
// 2. 模型文件目录存储JSON配置
fModelDir = new File(fUtilsDir, "ModelDir");
createDirWithPermission(fModelDir, "JSON配置目录应用外置存储");
// 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. 递归创建所有目录修改优先创建CropInner目录确保权限初始化
createDirWithPermission(fModelDir, "模型文件目录");
createDirWithPermission(fBackgroundSourceDir, "背景图片目录");
createDirWithPermission(fCropInnerDir, "优先裁剪目录BackgroundSource下"); // 优先创建
createDirWithPermission(fCropTempDir, "裁剪临时目录FileProvider适配");
createDirWithPermission(fCropFallbackDir, "裁剪兜底目录");
// 4. 初始化Bean文件对象存储JSON配置
// 3. 初始化JSON文件对象
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
LogUtils.d(TAG, "文件管理】所有目录初始化完成:目录=" + fUtilsDir.getAbsolutePath() + ",优先裁剪目录=" + fCropInnerDir.getAbsolutePath());
LogUtils.d(TAG, "JSON目录初始化完成:目录=" + fModelDir.getAbsolutePath());
}
/**
* 初始化所有文件(修改:新增优先裁剪文件初始化
* 包含:优先裁剪临时文件、原裁剪临时文件、裁剪结果文件
* 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录
*/
private void initAllFiles() {
// 1. 新增:优先裁剪临时文件(BackgroundSource/CropInner下FileProvider已配置
cropInnerTempFile = new File(fCropInnerDir, CROP_TEMP_FILE_NAME);
// 2. 裁剪临时文件(兼容旧逻辑
cropTempFile = new File(fCropTempDir, CROP_TEMP_FILE_NAME);
// 3. 裁剪结果文件(裁剪后保存的最终文件,存入背景图片目录)
// 1. 裁剪临时文件(图片基础目录/cache下系统裁剪可读写
cropTempFile = new File(fPictureCacheDir, CROP_TEMP_FILE_NAME);
// 2. 裁剪结果文件(图片存储目录下,最终保存的裁剪图
cropResultFile = new File(fBackgroundSourceDir, CROP_RESULT_FILE_NAME);
// 4. 初始化时清理旧文件(避免文件锁定/权限残留)
clearOldFile(cropInnerTempFile, "优先裁剪临时文件"); // 清理优先裁剪文件
clearOldFile(cropTempFile, "旧裁剪临时文件");
clearOldFile(cropResultFile, "旧裁剪结果文件");
// 3. 初始化时清理旧文件(避免文件锁定/权限残留)
clearOldFile(cropTempFile, "旧裁剪临时文件/Pictures/PowerBell/cache");
clearOldFile(cropResultFile, "旧裁剪结果文件(/Pictures/PowerBell/BackgroundSource");
LogUtils.d(TAG, "【文件管理】所有文件初始化完成:优先裁剪临时文件=" + cropInnerTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath());
LogUtils.d(TAG, "【文件初始化完成:裁剪临时文件=" + cropTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath());
}
/**
* 核心函数为系统裁剪应用创建可读写的FileProvider路径修改优先使用CropInner目录
* 适配Android14+、MIUI解决Permission denied+裁剪文件为0字节问题
* @return 裁剪临时文件File系统裁剪应用可读写路径已适配FileProvider
* 核心函数为系统裁剪应用创建可读写的FileProvider路径适配Android14+ MIUI
* 裁剪文件统一放入 /Pictures/PowerBell/cache/,确保系统裁剪工具可读写
*/
public File createCropFileProviderPath() {
LogUtils.d(TAG, "【裁剪路径】createCropFileProviderPath 触发,创建系统裁剪可读写路径");
// 1. 优先使用BackgroundSource下的CropInner目录核心修改权限可控FileProvider已配置
if (fCropInnerDir != null && fCropInnerDir.exists() && isDirActuallyWritable(fCropInnerDir)) {
// 优先使用图片基础目录下的cache目录核心系统公共目录权限更友好
if (fPictureCacheDir != null && fPictureCacheDir.exists() && isDirActuallyWritable(fPictureCacheDir)) {
try {
// 重新初始化优先裁剪临时文件(先删后建,避免文件锁定
clearOldFile(cropInnerTempFile, "优先裁剪临时文件(重新初始化)");
cropInnerTempFile.createNewFile();
// 强制设置文件权限(系统裁剪应用必需:允许所有用户读写)
setFilePermissions(cropInnerTempFile);
// 关键将当前裁剪文件指向优先裁剪文件上层Activity直接使用
cropTempFile = cropInnerTempFile;
LogUtils.d(TAG, "【裁剪路径】系统裁剪可读写路径创建成功(优先裁剪目录):" + cropTempFile.getAbsolutePath());
// 先清理旧文件,避免锁定
clearOldFile(cropTempFile, "裁剪临时文件(重新初始化)");
// 创建新的裁剪临时文件
cropTempFile.createNewFile();
// 强制设置权限(系统裁剪应用必需:允许所有用户读写)
setFilePermissions(cropTempFile);
LogUtils.d(TAG, "【裁剪路径】创建成功(/Pictures/PowerBell/cache" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】优先裁剪目录创建文件失败:" + e.getMessage(), e);
LogUtils.e(TAG, "【裁剪路径】cache目录创建文件失败:" + e.getMessage(), e);
}
} else {
LogUtils.w(TAG, "【裁剪路径】优先裁剪目录不可用(不存在或无权限),切换到备用目录");
LogUtils.w(TAG, "【裁剪路径】cache目录不可用切换到图片存储目录兜底");
}
// 2. 备用尝试裁剪临时目录FileProvider适配路径
if (isDirActuallyWritable(fCropTempDir)) {
// 兜底:使用图片存储目录(极端情况
if (isDirActuallyWritable(fBackgroundSourceDir)) {
try {
clearOldFile(cropTempFile, "裁剪临时文件(重新初始化)");
cropTempFile.createNewFile();
setFilePermissions(cropTempFile);
LogUtils.d(TAG, "【裁剪路径】系统裁剪可读写路径创建成功(裁剪临时目录):" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】裁剪临时目录创建文件失败:" + e.getMessage(), e);
}
}
// 3. 兜底1裁剪兜底目录
if (isDirActuallyWritable(fCropFallbackDir)) {
try {
cropTempFile = new File(fCropFallbackDir, CROP_TEMP_FILE_NAME);
cropTempFile = new File(fBackgroundSourceDir, CROP_TEMP_FILE_NAME);
clearOldFile(cropTempFile, "裁剪临时文件(兜底目录)");
cropTempFile.createNewFile();
setFilePermissions(cropTempFile);
LogUtils.w(TAG, "【裁剪路径】裁剪临时目录失败,切换到兜底目录创建成功:" + cropTempFile.getAbsolutePath());
LogUtils.w(TAG, "【裁剪路径】切换到图片存储目录创建成功:" + cropTempFile.getAbsolutePath());
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】裁剪兜底目录创建文件失败:" + e.getMessage(), e);
LogUtils.e(TAG, "【裁剪路径】图片存储目录创建文件失败:" + e.getMessage(), e);
}
}
// 4. 终极兜底:应用内部缓存目录(提示用户权限问题)
// 终极兜底:应用内部缓存目录(提示用户权限问题)
File cacheDir = mContext.getCacheDir();
if (isDirActuallyWritable(cacheDir)) {
try {
@@ -199,23 +182,22 @@ public class BackgroundSourceUtils {
clearOldFile(cropTempFile, "裁剪临时文件(终极兜底)");
cropTempFile.createNewFile();
setFilePermissions(cropTempFile);
LogUtils.w(TAG, "【裁剪路径】应用外部目录全部失败,终极兜底到缓存目录MIUI可能裁剪失败" + cropTempFile.getAbsolutePath());
LogUtils.w(TAG, "【裁剪路径】系统公共目录失败,兜底到应用缓存MIUI可能裁剪失败" + cropTempFile.getAbsolutePath());
ToastUtils.show("存储权限受限,建议授予「所有文件访问权限」以确保裁剪正常");
return cropTempFile;
} catch (IOException e) {
LogUtils.e(TAG, "【裁剪路径】终极兜底目录创建文件失败:" + e.getMessage(), e);
LogUtils.e(TAG, "【裁剪路径】终极兜底目录创建失败:" + e.getMessage(), e);
ToastUtils.show("裁剪路径创建失败,请重启应用并授予存储权限");
}
}
// 极端情况:所有目录均失败返回null上层需处理
LogUtils.e(TAG, "【裁剪路径】所有目录均无法创建裁剪文件,系统裁剪功能不可用");
// 极端情况:所有目录均失败
LogUtils.e(TAG, "【裁剪路径】所有目录均无法创建裁剪文件,裁剪功能不可用");
return null;
}
/**
* 加载背景图片配置数据(正式/预览Bean
* 从JSON文件读取若文件不存在则创建默认Bean并保存
* 加载背景图片配置数据(JSON文件仍在应用外置存储不改变
*/
void loadSettings() {
// 加载正式Bean
@@ -235,52 +217,64 @@ public class BackgroundSourceUtils {
}
}
/**
* 获取正式背景Bean对外提供用于修改正式配置
*/
// ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------
public BackgroundBean getCurrentBackgroundBean() {
return currentBackgroundBean;
}
/**
* 获取预览背景Bean对外提供用于修改预览配置
*/
public BackgroundBean getPreviewBackgroundBean() {
return previewBackgroundBean;
}
/**
* 获取正式背景图片路径(拼接:背景目录+正式Bean中的文件名
*/
public String getCurrentBackgroundFilePath() {
loadSettings(); // 加载最新配置,避免数据滞后
File file = new File(fBackgroundSourceDir, currentBackgroundBean.getBackgroundFileName());
LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验
*/
public String getCurrentBackgroundFilePath() {
// 移除:loadSettings(); // 关键修复避免每次调用都重新加载Bean导致字段被重置为空
String fileName = currentBackgroundBean.getBackgroundFileName();
// 强化校验:若文件名为空,返回空路径(避免拼接目录路径)
if (TextUtils.isEmpty(fileName)) {
LogUtils.e(TAG, "【路径管理】正式背景文件名为空,返回空路径");
return "";
}
File file = new File(fBackgroundSourceDir, fileName);
LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验)
*/
public String getPreviewBackgroundFilePath() {
// 移除loadSettings(); // 关键修复避免每次调用都重新加载Bean导致字段被重置为空
String fileName = previewBackgroundBean.getBackgroundFileName();
// 强化校验:若文件名为空,返回空路径(避免拼接目录路径)
if (TextUtils.isEmpty(fileName)) {
LogUtils.e(TAG, "【路径管理】预览背景文件名为空,返回空路径");
return "";
}
File file = new File(fBackgroundSourceDir, fileName);
LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验)
*/
public String getPreviewBackgroundScaledCompressFilePath() {
// 移除loadSettings(); // 关键修复
String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径");
return "";
}
File file = new File(fBackgroundSourceDir, compressFileName);
LogUtils.d(TAG, "【路径管理】预览压缩背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 获取预览背景图片路径(拼接:背景目录+预览Bean中的文件名
*/
public String getPreviewBackgroundFilePath() {
loadSettings(); // 加载最新配置,避免数据滞后
File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundFileName());
LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 获取预览背景压缩图片路径(拼接:背景目录+预览Bean中的压缩文件名
*/
public String getPreviewBackgroundScaledCompressFilePath() {
loadSettings(); // 加载最新配置,避免数据滞后
File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundScaledCompressFileName());
LogUtils.d(TAG, "【路径管理】预览压缩背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 保存配置(将正式/预览Bean同步到JSON文件持久化存储
* 保存配置JSON文件仍写入应用外置存储
*/
public void saveSettings() {
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
@@ -289,39 +283,27 @@ public class BackgroundSourceUtils {
}
/**
* 获取背景图片源目录路径(对外提供,用于创建临时文件
* 获取图片基础目录路径(对外提供/Pictures/PowerBell/
*/
public String getBackgroundSourceDirPath() {
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;
}
// ------------------------------ 工具方法(适配新目录权限)------------------------------
/**
* 新增:流复制文件(核心修复:Android14+ 共享存储权限限制适配
* 不依赖文件路径直接通过流复制支持读取相册私有隐藏文件避免Permission denied
* @param source 源文件(可为共享存储私有文件)
* @param target 目标文件(应用私有目录,确保可写)
* @return true=复制成功false=失败
* 流复制文件(适配系统公共目录,解决Android14+ 权限问题
*/
public boolean copyFileByStream(File source, File target) {
if (source == null || !source.exists() || !source.isFile() || target == null) {
@@ -329,40 +311,46 @@ public class BackgroundSourceUtils {
return false;
}
// 确保目标目录存在
// 确保目标目录存在(系统公共目录需强制创建,适配/Pictures/PowerBell/路径)
File targetDir = target.getParentFile();
if (!targetDir.exists()) {
createDirWithPermission(targetDir, "流复制目标目录");
createDirWithPermission(targetDir, "流复制目标目录/Pictures/PowerBell下");
}
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 打开源文件输入流(支持共享存储私有文件)
// 打开源文件输入流(支持共享存储私有文件/系统公共目录文件
fis = new FileInputStream(source);
// 打开目标文件输出流
// 打开目标文件输出流(适配/Pictures/PowerBell目录权限
fos = new FileOutputStream(target);
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区提升复制效率
int len;
// 循环读取流并写入目标文件Java7 普通for循环兼容语法)
// 循环读取流并写入目标文件Java7 兼容语法)
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
fos.getFD().sync(); // 强制同步到磁盘,确保文件写入完成Java7 支持)
fos.getFD().sync(); // 强制同步到磁盘,避免系统公共目录缓存导致文件损坏
LogUtils.d(TAG, "【文件管理】流复制成功:" + source.getAbsolutePath() + "" + target.getAbsolutePath() + ",大小:" + target.length() + "bytes");
// 复制成功后强制设置目标文件权限(确保后续裁剪/预览可读写)
setFilePermissions(target);
return true;
} catch (Exception e) {
LogUtils.e(TAG, "【文件管理】流复制异常:" + e.getMessage(), e);
// 复制失败时删除目标文件(避免残留空文件)
LogUtils.e(TAG, "【文件管理】流复制异常/Pictures/PowerBell目录" + e.getMessage(), e);
// 复制失败时删除目标文件(避免残留空文件导致后续逻辑异常
if (target.exists()) {
clearOldFileByExternal(target, "流复制失败残留文件");
}
// 针对系统公共目录权限异常,给出明确提示
if (e instanceof SecurityException || e.getMessage().contains("EACCES")) {
ToastUtils.show("图片复制失败,请授予应用「存储权限」和「所有文件访问权限」");
}
return false;
} finally {
// 关闭流资源Java7 手动关闭,避免内存泄漏,不使用try-with-resources
// 关闭流资源Java7 手动关闭,避免内存泄漏,不依赖try-with-resources
if (fis != null) {
try {
fis.close();
@@ -381,29 +369,29 @@ public class BackgroundSourceUtils {
}
/**
* 保存图片到预览Bean核心修复:替换路径复制为流复制,避免预览路径错误
* 保存图片到预览Bean图片存储到/Pictures/PowerBell/BackgroundSourceJSON仍存应用外置存储
* @param sourceFile 源图片文件(非空,必须存在)
* @param fileInfo 图片附加信息如Uri字符串仅作备注
* @return 更新后的预览Bean
*/
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
// 强化校验:源文件必须存在、是文件、大小>0
// 强化校验:源文件必须存在、是文件、大小>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;
}
// 确保背景目录存在(防止首次使用时目录未创建
// 确保图片存储目录存在(/Pictures/PowerBell/BackgroundSource
if (!fBackgroundSourceDir.exists()) {
createDirWithPermission(fBackgroundSourceDir, "背景图片目录saveFileToPreviewBean");
createDirWithPermission(fBackgroundSourceDir, "图片存储目录saveFileToPreviewBean");
}
// 生成唯一文件名(基于源文件后缀,避免重复)
// 生成唯一文件名(基于源文件后缀,避免重复,适配系统公共目录
String uniqueFileName = FileUtils.createUniqueFileName(sourceFile);
File previewBackgroundFile = new File(fBackgroundSourceDir, uniqueFileName);
// 核心修改用流复制替代原FileUtils.copyFile解决共享存储权限问题
// 核心用流复制替代原FileUtils.copyFile适配/Pictures/PowerBell目录权限
boolean copySuccess = copyFileByStream(sourceFile, previewBackgroundFile);
if (!copySuccess) {
LogUtils.e(TAG, "【文件管理】图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + "" + previewBackgroundFile.getAbsolutePath());
@@ -411,20 +399,23 @@ public class BackgroundSourceUtils {
return previewBackgroundBean;
}
// 正确赋值预览Bean确保文件名非空,避免后续路径为空
previewBackgroundBean = new BackgroundBean();
previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 唯一文件名(非空)
previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_" + previewBackgroundFile.getName()); // 压缩文件名(前缀标识)
previewBackgroundBean.setBackgroundFileInfo(fileInfo); // 附加信息Uri
previewBackgroundBean.setIsUseBackgroundFile(true); // 标记使用背景图
previewBackgroundBean.setIsUseScaledCompress(true); // 启用压缩图
previewBackgroundBean.setBackgroundWidth(100); // 默认宽高比1:1
previewBackgroundBean.setBackgroundHeight(100);
saveSettings(); // 持久化保存预览Bean
// 正确赋值预览Bean确保文件名非空
previewBackgroundBean = new BackgroundBean();
previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 唯一文件名(非空)
previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_" + previewBackgroundFile.getName());
previewBackgroundBean.setBackgroundFileInfo(fileInfo);
previewBackgroundBean.setIsUseBackgroundFile(true);
previewBackgroundBean.setIsUseScaledCompress(true);
previewBackgroundBean.setBackgroundWidth(100);
previewBackgroundBean.setBackgroundHeight(100);
LogUtils.d(TAG, "【文件管理】预览图片保存成功:" + previewBackgroundFile.getAbsolutePath() + ",大小:" + previewBackgroundFile.length() + "bytes");
ToastUtils.show("预览图片加载成功");
return previewBackgroundBean;
// 关键强化强制保存Bean到JSON确保后续loadSettings()能加载到有效Bean
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "【文件管理】预览Bean强制保存到JSON" + previewBackgroundBeanFile.getAbsolutePath());
LogUtils.d(TAG, "【文件管理】预览图片保存成功(/Pictures/PowerBell" + previewBackgroundFile.getAbsolutePath() + ",大小:" + previewBackgroundFile.length() + "bytes");
ToastUtils.show("预览图片加载成功");
return previewBackgroundBean;
}
/**
@@ -442,8 +433,8 @@ public class BackgroundSourceUtils {
currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight());
currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor());
saveSettings(); // 持久化保存正式Bean
LogUtils.d(TAG, "【配置管理】预览背景提交成功:正式背景更新为预览背景");
saveSettings(); // 持久化保存正式BeanJSON写入应用外置存储
LogUtils.d(TAG, "【配置管理】预览背景提交成功:正式背景更新为/Pictures/PowerBell下的预览背景");
ToastUtils.show("背景图片应用成功");
}
@@ -462,12 +453,12 @@ public class BackgroundSourceUtils {
previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight());
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
saveSettings(); // 持久化保存预览Bean
LogUtils.d(TAG, "【配置管理】正式背景同步到预览:预览背景更新为当前正式背景");
saveSettings(); // 持久化保存预览BeanJSON写入应用外置存储
LogUtils.d(TAG, "【配置管理】正式背景同步到预览:预览背景更新为/Pictures/PowerBell下的正式背景");
}
/**
* 工具方法:创建目录并设置权限(确保目录可读写适配Android14+
* 工具方法:创建目录并设置权限(适配系统公共目录/Pictures/PowerBell确保可读写
* @param dir 要创建的目录
* @param dirDesc 目录描述(用于日志打印)
*/
@@ -488,12 +479,12 @@ public class BackgroundSourceUtils {
}
}
}
// 强制设置目录权限(递归设置,确保所有层级可读写)
// 强制设置目录权限(递归设置,确保系统公共目录所有层级可读写)
setDirPermissionsRecursively(dir);
}
/**
* 工具方法:递归设置目录及子目录/文件的读写权限(适配Android全版本
* 工具方法:递归设置目录及子目录/文件的读写权限(适配系统公共目录/Pictures/PowerBell
* @param dir 要设置权限的目录
*/
private void setDirPermissionsRecursively(File dir) {
@@ -503,7 +494,7 @@ public class BackgroundSourceUtils {
return;
}
try {
// 设置目录权限(允许所有用户读写,系统裁剪应用必需)
// 设置目录权限(允许所有用户读写,系统裁剪应用/预览功能必需)
dir.setReadable(true, false);
dir.setWritable(true, false);
dir.setExecutable(false, false);
@@ -518,27 +509,28 @@ public class BackgroundSourceUtils {
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());
// 裁剪/预览相关文件单独打印日志
if (file.getName().contains(CROP_TEMP_FILE_NAME) || file.getName().contains(CROP_RESULT_FILE_NAME) || file.getName().startsWith("ScaledCompress_")) {
LogUtils.d(TAG, "【权限管理】关键文件权限设置:文件名=" + file.getName() + ",可写=" + file.canWrite());
}
}
}
}
} catch (SecurityException e) {
LogUtils.e(TAG, "【权限管理】设置目录权限失败(系统禁止):" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
ToastUtils.show("目录权限设置失败,请授予应用存储权限");
} catch (Exception e) {
LogUtils.e(TAG, "【权限管理】设置目录权限异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e);
}
}
/**
* 工具方法:设置单个文件权限(确保系统裁剪应用可读写
* 【关键调整】修改为public适配BackgroundSettingsActivity的外部调用
* 工具方法:设置单个文件权限(确保系统裁剪应用/预览功能可读写,适配/Pictures/PowerBell目录
* 【关键调整】public修饰适配BackgroundSettingsActivity的外部调用
* @param file 要设置权限的文件
*/
public void setFilePermissions(File file) {
@@ -551,14 +543,14 @@ public class BackgroundSourceUtils {
file.setReadable(true, false);
file.setWritable(true, false);
file.setExecutable(false, false);
LogUtils.d(TAG, "【权限管理】文件权限设置完成:路径=" + file.getAbsolutePath() + ",可写=" + file.canWrite() + ",可读=" + file.canRead());
LogUtils.d(TAG, "【权限管理】文件权限设置完成/Pictures/PowerBell下:路径=" + file.getAbsolutePath() + ",可写=" + file.canWrite() + ",可读=" + file.canRead());
} catch (Exception e) {
LogUtils.e(TAG, "【权限管理】设置文件权限失败:" + file.getAbsolutePath() + ",错误:" + e.getMessage(), e);
}
}
/**
* 工具方法:清理旧文件(避免文件锁定/残留)【内部私有,不对外暴露】
* 工具方法:清理旧文件(避免文件锁定/残留,适配系统公共目录)【内部私有,不对外暴露】
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
@@ -567,6 +559,8 @@ public class BackgroundSourceUtils {
return;
}
if (file.exists()) {
// 先设置文件为可写(避免系统公共目录下文件只读导致删除失败)
file.setWritable(true, false);
boolean deleteSuccess = file.delete();
LogUtils.d(TAG, "【文件管理】清理" + fileDesc + "" + (deleteSuccess ? "成功" : "失败") + ",路径:" + file.getAbsolutePath());
// 若删除失败,标记为退出时删除(兼容文件锁定场景)
@@ -578,7 +572,7 @@ public class BackgroundSourceUtils {
}
/**
* 工具方法验证目录实际写入能力解决Android14+ canWrite()假阳性问题)
* 工具方法验证目录实际写入能力解决Android14+ canWrite()假阳性问题,适配/Pictures/PowerBell
* 原理:通过创建临时空文件并删除,验证目录是否真的可写
* @param dir 要验证的目录
* @return true=实际可写false=实际不可写
@@ -595,7 +589,7 @@ public class BackgroundSourceUtils {
boolean canWrite = testFile.canWrite();
boolean canRead = testFile.canRead();
testFile.delete(); // 删除临时文件,不占用空间
LogUtils.d(TAG, "【权限校验】目录实际写入校验:" + dir.getAbsolutePath() + ",创建成功=" + createSuccess + ",可写=" + canWrite + ",可读=" + canRead + ",结果=" + (canWrite ? "通过" : "失败"));
LogUtils.d(TAG, "【权限校验】目录实际写入校验/Pictures/PowerBell下" + dir.getAbsolutePath() + ",创建成功=" + createSuccess + ",可写=" + canWrite + ",可读=" + canRead + ",结果=" + (canWrite ? "通过" : "失败"));
return canWrite;
} else {
LogUtils.d(TAG, "【权限校验】目录实际写入校验失败:" + dir.getAbsolutePath() + "创建临时文件失败Permission denied");
@@ -608,75 +602,65 @@ public class BackgroundSourceUtils {
}
/**
* 工具方法复制文件适配大文件避免OOM
* 【关键优化】兼容源文件为空的场景(适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用
* 工具方法复制文件适配大文件避免OOM,兼容源文件为空场景
* 【关键优化】适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用
* @param source 源文件(可为空,为空时仅创建目标目录)
* @param target 目标文件/目录若源文件为空target视为目录并创建
* @return true=复制/创建成功false=失败
*/
public boolean copyFile(File source, File target) {
// 场景1源文件为空 → 仅创建目标目录适配Activity的目录创建调用)
// 场景1源文件为空 → 仅创建目标目录适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用)
if (source == null || (source.exists() && source.length() <= 0)) {
if (target == null) {
LogUtils.e(TAG, "【文件管理】目录创建失败目标目录对象为null");
return false;
}
// 若target是文件取其父目录若本身是目录直接创建
// 若target是文件取其父目录若本身是目录直接创建(适配/Pictures/PowerBell目录
File targetDir = target.isFile() ? target.getParentFile() : target;
createDirWithPermission(targetDir, "空源文件场景-目录创建");
createDirWithPermission(targetDir, "空源文件场景-目录创建/Pictures/PowerBell下");
LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath());
return true;
}
// 场景2正常文件复制源文件非空且存在
// 场景2正常文件复制源文件非空且存在,适配系统公共目录/Pictures/PowerBell
if (!source.exists() || target == null) {
LogUtils.e(TAG, "【文件管理】文件复制失败:源文件无效或目标文件为空");
return false;
}
// 确保目标目录存在
// 确保目标目录存在(系统公共目录需强制创建,避免权限问题)
File targetDir = target.getParentFile();
if (!targetDir.exists()) {
createDirWithPermission(targetDir, "文件复制目标目录");
createDirWithPermission(targetDir, "文件复制目标目录/Pictures/PowerBell下");
}
// 调用FileUtils复制若项目中已有该方法可直接复用
return FileUtils.copyFile(source, target);
// 调用流复制方法适配系统公共目录权限避免Permission denied
return copyFileByStream(source, target);
}
/**
* 工具方法清理裁剪相关临时文件对外提供Activity退出时调用
* 工具方法清理裁剪相关临时文件对外提供Activity退出时调用,适配/Pictures/PowerBell/cache目录
*/
public void clearCropTempFiles() {
clearOldFile(cropTempFile, "裁剪临时文件");
clearOldFile(cropResultFile, "裁剪结果文件");
// 清理裁剪目录下的其他临时文件Java7 普通for循环
if (fCropTempDir.exists()) {
File[] files = fCropTempDir.listFiles();
clearOldFile(cropTempFile, "裁剪临时文件/Pictures/PowerBell/cache");
clearOldFile(cropResultFile, "裁剪结果文件/Pictures/PowerBell/BackgroundSource");
// 清理裁剪缓存目录下的其他临时文件Java7 普通for循环,兼容语法
if (fPictureCacheDir.exists()) {
File[] files = fPictureCacheDir.listFiles();
if (files != null && files.length > 0) {
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isFile()) {
// 强制设置为可写后删除(避免系统公共目录文件只读)
file.setWritable(true, false);
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();
}
}
}
}
LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成");
LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成(/Pictures/PowerBell下");
}
/**
* 对外接口清理指定旧文件适配BackgroundSettingsActivity调用
* 对外接口清理指定旧文件适配BackgroundSettingsActivity调用,支持/Pictures/PowerBell目录
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
@@ -685,7 +669,7 @@ public class BackgroundSourceUtils {
}
/**
* 工具方法:获取目录类型描述(用于日志调试,明确目录类型)
* 工具方法:获取目录类型描述(用于日志调试,明确目录类型,适配新目录结构
* @param dir 目标目录
* @return 目录类型描述
*/
@@ -694,11 +678,14 @@ public class BackgroundSourceUtils {
return "未知目录null";
}
String dirPath = dir.getAbsolutePath();
String publicPicturePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).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(),系统裁剪可读写";
if (!TextUtils.isEmpty(publicPicturePath) && dirPath.contains(publicPicturePath + File.separator + "PowerBell")) {
return "系统公共图片目录(/Pictures/PowerBell图片存储/裁剪目录";
} else if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) {
return "应用私有外部目录getExternalFilesDir()JSON配置目录";
} else if (dirPath.contains(cachePath)) {
return "应用内部缓存目录getCacheDir(),兜底目录)";
} else {

View File

@@ -55,5 +55,12 @@
<files-path
name="app_internal_files"
path="." /> <!-- path="." 表示映射整个应用内目录 -->
<!-- 关键新增:系统公共图片目录 /Pictures/PowerBell图片存储/裁剪目录) -->
<external-path
name="public_pictures_powerbell"
path="Pictures/PowerBell/" /> <!-- 路径:/storage/emulated/0/Pictures/PowerBell/ -->
<!-- 兜底:应用内部缓存目录 -->
<cache-path
name="cache_path"
path="BackgroundSourceUtils/" />
</paths>