图片剪裁调试完成
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/PowerBell,JSON→应用外置存储)
|
||||
*/
|
||||
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)
|
||||
// 加载配置(正式/预览Bean,JSON目录下)
|
||||
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/BackgroundSource,JSON仍存应用外置存储)
|
||||
* @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(); // 持久化保存正式Bean(JSON写入应用外置存储)
|
||||
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(); // 持久化保存预览Bean(JSON写入应用外置存储)
|
||||
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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user