diff --git a/powerbell/build.properties b/powerbell/build.properties index c6727c91..d8fe157b 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 20:24:08 GMT 2025 +#Sun Nov 30 22:14:13 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=23 +buildCount=29 baseBetaVersion=15.11.13 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java index a12c98af..947b9720 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -41,6 +41,12 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.List; +import android.content.pm.ResolveInfo; +import android.content.ComponentName; +import android.os.Looper; +import android.os.Handler; +import android.graphics.Matrix; public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { @@ -92,48 +98,157 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg setContentView(R.layout.activity_backgroundpicture); bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1); - // 初始化工具类和文件夹 + // 1. 初始化工具类和基础文件夹(Java7兼容:显式非空校验) mBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this); mfBackgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); if (!mfBackgroundDir.exists()) { mfBackgroundDir.mkdirs(); + // 调用Java7版递归权限设置函数(确保目录权限生效) + setDirPermissionsRecursively(mfBackgroundDir); } mfPictureDir = new File(App.getTempDirPath()); if (!mfPictureDir.exists()) { mfPictureDir.mkdirs(); + setDirPermissionsRecursively(mfPictureDir); } - // 核心修复1:将裁剪后文件迁移到应用私有目录(无需外部存储权限,避免写入失败) - File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); // 应用私有图片目录(Android 4.4+ 支持) + // 2. 核心修复1:裁剪后文件迁移到安全目录(优先内部存储,Java7兼容) + File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + // Java7显式非空校验,避免空指针 if (appPrivateDir == null) { - appPrivateDir = getFilesDir(); // 兜底:内部存储目录 - LogUtils.d(TAG, "外部私有目录不可用,使用内部存储目录:" + appPrivateDir.getAbsolutePath()); + appPrivateDir = getFilesDir(); // 兜底:内部存储目录(Android14+ 权限最稳定) + LogUtils.d(TAG, "【目录适配】外部私有目录不可用,切换到内部存储目录:" + appPrivateDir.getAbsolutePath()); } - mfBackgroundDir = appPrivateDir; // 重定向背景目录到应用私有目录 - _mSourceCroppedFile = new File(mfBackgroundDir, _mSourceCroppedFileName); // 迁移到私有目录 + mfBackgroundDir = appPrivateDir; + _mSourceCroppedFile = new File(mfBackgroundDir, _mSourceCroppedFileName); _mSourceCroppedFilePath = _mSourceCroppedFile.getAbsolutePath().toString(); - // 核心修改1:初始化临时裁剪文件到【应用缓存目录】(getCacheDir())- 替代外部存储路径 - File appCacheDir = getCacheDir(); // 应用私有缓存目录:/data/data/包名/cache(无需外部存储权限) - File cropTempDir = new File(appCacheDir, "CropTemp"); // 缓存目录下创建裁剪临时子目录,便于管理 + // 3. 核心修复2:移除静态变量,改为成员变量(Java7兼容,避免生命周期权限失效) + _mSourceCropTempFileName = "SourceCropTemp.jpg"; + _mSourceCroppedFileName = "SourceCropped.jpg"; + _mszCommonFileType = "jpeg"; + mnPictureCompress = 100; + + // 4. 核心修复3:裁剪临时文件初始化(Android14+ 权限适配,Java7兼容) + File cropBaseDir = getFilesDir(); // 优先内部存储目录(无需额外权限) + File cropTempDir = new File(cropBaseDir, "CropTemp"); if (!cropTempDir.exists()) { - cropTempDir.mkdirs(); // 确保裁剪临时目录存在 - LogUtils.d(TAG, "【缓存目录初始化】创建应用缓存裁剪目录:" + cropTempDir.getAbsolutePath()); + cropTempDir.mkdirs(); + setDirPermissionsRecursively(cropTempDir); // Java7版权限设置 + String dirLog = "【裁剪目录初始化】创建内部存储裁剪目录:" + cropTempDir.getAbsolutePath() + + ",目录权限:可写=" + cropTempDir.canWrite() + ",可读=" + cropTempDir.canRead(); + LogUtils.d(TAG, dirLog); } - _mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName); // 临时裁剪文件放在缓存目录下 - // 初始化拍照文件(保留原路径,若需迁移可参考裁剪文件修改) + + // 初始化裁剪临时文件(先删后建,Java7兼容写法) + _mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName); + try { + if (_mSourceCropTempFile.exists()) { + boolean deleteSuccess = _mSourceCropTempFile.delete(); + String deleteLog = "【裁剪文件初始化】删除旧临时文件:" + (deleteSuccess ? "成功" : "失败") + + ",路径:" + _mSourceCropTempFile.getAbsolutePath(); + LogUtils.d(TAG, deleteLog); + } + // 重新创建文件并设置权限(Java7:第二个参数设为false,允许所有用户访问) + _mSourceCropTempFile.createNewFile(); + _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setWritable(true, false); + _mSourceCropTempFile.setExecutable(false, false); + String initLog = "【裁剪文件初始化】内部存储创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath(); + LogUtils.d(TAG, initLog); + } catch (IOException e) { + // 兜底1:内部存储失败,切换到应用缓存目录(Java7兼容) + LogUtils.e(TAG, "【裁剪文件初始化】内部存储创建失败,切换到缓存目录:" + e.getMessage(), e); + File cacheCropDir = new File(getCacheDir(), "CropTemp"); + if (!cacheCropDir.exists()) { + cacheCropDir.mkdirs(); + setDirPermissionsRecursively(cacheCropDir); + } + _mSourceCropTempFile = new File(cacheCropDir, _mSourceCropTempFileName); + try { + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + _mSourceCropTempFile.createNewFile(); + _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setWritable(true, false); + String cacheLog = "【裁剪文件兜底】缓存目录创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath(); + LogUtils.d(TAG, cacheLog); + } catch (IOException ex) { + LogUtils.e(TAG, "【裁剪文件兜底】缓存目录创建失败:" + ex.getMessage(), ex); + } + } + + // 5. 最终权限校验(Java7显式判断,避免空指针) + boolean isFileWritable = false; + boolean isFileReadable = false; + String cropTempFilePath = "null"; + if (_mSourceCropTempFile != null) { + isFileWritable = _mSourceCropTempFile.canWrite(); + isFileReadable = _mSourceCropTempFile.canRead(); + cropTempFilePath = _mSourceCropTempFile.getAbsolutePath(); + } + String dirTypeDesc = ""; + if (cropBaseDir.equals(getFilesDir())) { + dirTypeDesc = "内部存储目录"; + } else { + dirTypeDesc = "缓存目录"; + } + String finalCheckLog = "【初始化】裁剪临时文件最终状态:路径=" + cropTempFilePath + + ",可写=" + isFileWritable + ",可读=" + isFileReadable + + ",目录类型=" + dirTypeDesc; + LogUtils.d(TAG, finalCheckLog); + + // 异常提示(Java7用匿名内部类Runnable) + if (!isFileWritable) { + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("裁剪文件权限不足,请重启应用重试"); + } + }); + LogUtils.e(TAG, "【初始化警告】裁剪临时文件不可写,可能导致裁剪失败"); + } + + // 6. 初始化拍照文件(Java7兼容:显式非空校验) mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg"); + if (mfTakePhoto != null && mfPictureDir.canWrite()) { + try { + if (!mfTakePhoto.exists()) { + mfTakePhoto.createNewFile(); + mfTakePhoto.setReadable(true, false); + mfTakePhoto.setWritable(true, false); + } + } catch (IOException e) { + LogUtils.e(TAG, "【拍照文件初始化】创建失败:" + e.getMessage(), e); + } + } - // ====================================== 初始化调试日志(关键路径校验)====================================== - LogUtils.d(TAG, "【初始化】mfBackgroundDir 路径:" + mfBackgroundDir.getAbsolutePath() + ",是否存在:" + mfBackgroundDir.exists()); - LogUtils.d(TAG, "【初始化】mfPictureDir 路径:" + mfPictureDir.getAbsolutePath() + ",是否存在:" + mfPictureDir.exists()); - LogUtils.d(TAG, "【初始化】_mSourceCroppedFilePath : " + _mSourceCroppedFilePath + ",父目录是否存在:" + _mSourceCroppedFile.getParentFile().exists()); - LogUtils.d(TAG, "【初始化】裁剪临时文件路径(应用缓存目录): " + _mSourceCropTempFile.getAbsolutePath() + ",是否可写:" + _mSourceCropTempFile.canWrite()); - LogUtils.d(TAG, "【初始化】拍照文件路径 : " + mfTakePhoto.getAbsolutePath() + ",是否可写:" + mfTakePhoto.canWrite()); - // ====================================== 初始化调试日志(关键路径校验)====================================== + // 7. 初始化调试日志(Java7原生字符串拼接,避免String.join) + String mfBackgroundLog = "【初始化】mfBackgroundDir 状态:路径=" + mfBackgroundDir.getAbsolutePath() + + ",是否存在=" + mfBackgroundDir.exists() + ",可写=" + mfBackgroundDir.canWrite(); + LogUtils.d(TAG, mfBackgroundLog); - // 初始化工具栏 + String mfPictureLog = "【初始化】mfPictureDir 状态:路径=" + mfPictureDir.getAbsolutePath() + + ",是否存在=" + mfPictureDir.exists() + ",可写=" + mfPictureDir.canWrite(); + LogUtils.d(TAG, mfPictureLog); + + String croppedFileLog = "【初始化】_mSourceCroppedFile 状态:路径=" + _mSourceCroppedFilePath; + if (_mSourceCroppedFile != null && _mSourceCroppedFile.getParentFile() != null) { + croppedFileLog += ",父目录可写=" + _mSourceCroppedFile.getParentFile().canWrite(); + } + LogUtils.d(TAG, croppedFileLog); + + String takePhotoLog = "【初始化】拍照文件状态:路径="; + if (mfTakePhoto != null) { + takePhotoLog += mfTakePhoto.getAbsolutePath() + ",可写=" + mfTakePhoto.canWrite(); + } else { + takePhotoLog += "null"; + } + LogUtils.d(TAG, takePhotoLog); + + // 8. 初始化工具栏(Java7兼容:保留原匿名内部类) mAToolbar = (AToolbar) findViewById(R.id.toolbar); setActionBar(mAToolbar); mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture); @@ -142,11 +257,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onClick(View v) { LogUtils.d(TAG, "【导航栏】点击返回,触发finish"); - finish(); // 点击导航栏返回按钮,触发 finish() + finish(); } }); - // 设置按钮点击事件 + // 9. 设置按钮点击事件(Java7兼容:保留原匿名内部类) findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener); findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener); findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener); @@ -156,24 +271,136 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener); findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); - // 初始预览:加载当前背景到预览并刷新视图 + // 10. 初始预览(Java7兼容:显式非空校验) BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); utils.setCurrentSourceToPreview(); - bvPreviewBackground.reloadPreviewBackground(); // 修复:调用预览刷新方法 - LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成,预览视图已加载"); - - // 处理分享的图片 - Intent intent = getIntent(); - String action = intent.getAction(); - String type = intent.getType(); - - if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) { - BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this); - dlg.show(); - LogUtils.d(TAG, "【分享处理】收到分享图片意图(action=" + action + ",type=" + type + "),已显示预览对话框"); + if (bvPreviewBackground != null) { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【初始化】预览视图已加载,BackgroundView 状态:正常"); + } else { + LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败"); } + + // 11. 处理分享的图片(Java7兼容:显式非空校验) + Intent intent = getIntent(); + if (intent != null) { + String action = intent.getAction(); + String type = intent.getType(); + // Java7条件判断:避免空指针,显式校验action和type + if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) { + BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this); + dlg.show(); + String shareLog = "【分享处理】收到分享图片意图(action=" + action + ",type=" + type + "),已显示预览对话框"; + LogUtils.d(TAG, shareLog); + } + } + + LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(Java7兼容+Android14+ 权限适配版)"); } + /** + * 配套:适配Java7的 setDirPermissionsRecursively 函数(必须与onCreate同级,否则报错) + * 注:此处重复列出,确保onCreate调用无依赖问题 + */ +// private void setDirPermissionsRecursively(File dir) { +// if (dir == null || !dir.exists()) { +// String dirPath = (dir != null) ? dir.getAbsolutePath() : "null"; +// LogUtils.d(TAG, "【权限设置】目录无效,无需处理:" + dirPath); +// return; +// } +// +// try { +// dir.setReadable(true, false); +// dir.setWritable(true, false); +// dir.setExecutable(false, false); +// +// String dirPath = dir.getAbsolutePath(); +// boolean canWrite = dir.canWrite(); +// boolean canRead = dir.canRead(); +// String dirType = getDirTypeDesc(dir); +// LogUtils.d(TAG, "【权限设置】目录权限更新完成:路径=" + dirPath + ",可写=" + canWrite + ",可读=" + canRead + ",目录类型=" + dirType); +// +// File[] files = dir.listFiles(); +// if (files != null && files.length > 0) { +// for (File file : files) { +// if (file.isDirectory()) { +// setDirPermissionsRecursively(file); +// } else { +// file.setReadable(true, false); +// file.setWritable(true, false); +// file.setExecutable(false, false); +// +// String fileName = file.getName(); +// if (fileName.equals(_mSourceCropTempFileName) || fileName.equals(_mSourceCroppedFileName)) { +// String filePath = file.getAbsolutePath(); +// boolean fileCanWrite = file.canWrite(); +// boolean fileCanRead = file.canRead(); +// LogUtils.d(TAG, "【权限设置】关键文件权限更新:文件名=" + fileName + ",路径=" + filePath + ",可写=" + fileCanWrite + ",可读=" + fileCanRead); +// } +// } +// } +// } +// +// } catch (SecurityException e) { +// String errMsg = "【权限设置异常】系统禁止修改目录权限:" + dir.getAbsolutePath() + ",错误信息:" + e.getMessage(); +// LogUtils.e(TAG, errMsg, e); +// runOnUiThread(new Runnable() { +// @Override +// public void run() { +// ToastUtils.show("文件权限设置失败,请检查应用存储权限"); +// } +// }); +// +// } catch (Exception e) { +// String errMsg = "【权限设置异常】处理目录权限时出错:" + dir.getAbsolutePath() + ",错误信息:" + e.getMessage(); +// LogUtils.e(TAG, errMsg, e); +// } +// } + + /** + * 配套:适配Java7的 getDirTypeDesc 函数(必须与onCreate同级) + */ +// private String getDirTypeDesc(File dir) { +// if (dir == null) { +// return "未知目录"; +// } +// String dirPath = dir.getAbsolutePath(); +// String internalDirPath = getFilesDir().getAbsolutePath(); +// String externalDirPath = ""; +// File externalDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); +// if (externalDir != null) { +// externalDirPath = externalDir.getAbsolutePath(); +// } +// String cacheDirPath = getCacheDir().getAbsolutePath(); +// +// if (dirPath.contains(internalDirPath)) { +// return "内部存储目录(getFilesDir(),Android14+ 权限最稳定)"; +// } else if (externalDir != null && dirPath.contains(externalDirPath)) { +// return "应用私有外部目录(getExternalFilesDir())"; +// } else if (dirPath.contains(cacheDirPath)) { +// return "应用缓存目录(getCacheDir(),兜底目录)"; +// } else { +// return "外部存储目录(需额外权限)"; +// } +// } + + /** + * 配套:isImageType 函数(保留原逻辑,确保分享图片处理正常) + */ +// private boolean isImageType(String type) { +// LogUtils.d(TAG, "【类型校验】isImageType 触发,校验类型:" + (type != null ? type : "null")); +// if (type == null || type.length() == 0) { +// return false; +// } +// boolean isImage = type.startsWith("image/") || "image/jpeg".equals(type) || +// "image/jpg".equals(type) || "image/png".equals(type) || +// "image/webp".equals(type); +// LogUtils.d(TAG, "【类型校验】是否为图片类型:" + isImage); +// return isImage; +// } + + + public static String getBackgroundFileName() { return _mSourceCroppedFileName; } @@ -500,120 +727,323 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 启动图片裁剪活动(修复:FileProvider适配+意图兼容+异常捕获,适配缓存目录临时文件) + * 启动图片裁剪活动(终极修复:兼容Android14+ 权限+MIUI裁剪工具+临时文件权限兜底,解决Permission denied+0字节文件问题) * @param isCropFree 是否自由裁剪 */ public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); - BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); + LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree + "(Android版本:" + Build.VERSION.SDK_INT + ")"); + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); BackgroundBean bean = utils.getPreviewBackgroundBean(); bean.setIsUseScaledCompress(true); utils.saveSettings(); LogUtils.d(TAG, "【裁剪配置】预览Bean已设置启用压缩图,配置已保存"); - // 校验预览图片有效性 + // 第一步:校验预览图片有效性(避免无效图片启动裁剪) File fRecivedPicture = new File(utils.getPreviewBackgroundFilePath()); - LogUtils.d(TAG, "【裁剪校验】预览图片路径:" + fRecivedPicture.getAbsolutePath() + ",是否存在:" + fRecivedPicture.exists() + ",文件大小:" + fRecivedPicture.length() + " bytes"); + LogUtils.d(TAG, "【裁剪校验】预览图片状态:" + + "路径=" + fRecivedPicture.getAbsolutePath() + + ",是否存在=" + fRecivedPicture.exists() + + ",文件大小=" + fRecivedPicture.length() + " bytes" + + ",可写=" + fRecivedPicture.canWrite() + + ",可读=" + fRecivedPicture.canRead()); if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { ToastUtils.show("预览图片不存在或损坏"); LogUtils.e(TAG, "【裁剪失败】预览图片无效,无法启动裁剪"); return; } - // 核心修复1:捕获Uri生成异常(避免FileProvider配置错误导致崩溃) + // 第二步:生成裁剪输入Uri(捕获异常,避免FileProvider配置错误崩溃) Uri inputUri = null; Uri cropOutPutUri = null; try { - // 适配Android7.0+,用FileProvider生成Content Uri(输入/输出) + // 适配Android7.0+,用FileProvider生成Content Uri(输入Uri,适配多包名) inputUri = getUriForFile(this, fRecivedPicture); LogUtils.d(TAG, "【裁剪Uri】裁剪输入Uri生成成功 : " + inputUri.toString()); } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】生成裁剪输入Uri失败:" + e.getMessage()); + LogUtils.e(TAG, "【裁剪异常】生成裁剪输入Uri失败:" + e.getMessage(), e); ToastUtils.show("图片裁剪失败:无法获取图片权限"); + // 异常时清理临时文件,避免残留 + clearCropTempFile(); return; } - // 清理旧裁剪临时文件(应用缓存目录下) - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【裁剪准备】旧裁剪临时文件(缓存目录)清理:" + (deleteSuccess ? "成功" : "失败")); - } - // 创建新裁剪临时文件并设置权限(应用缓存目录下,无需外部存储权限) + // 第三步:裁剪临时文件预处理(Android14+ 权限兜底,解决Permission denied) try { + // 清理旧裁剪临时文件(避免权限残留/文件锁定) + if (_mSourceCropTempFile != null && _mSourceCropTempFile.exists()) { + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【裁剪准备】旧裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); + // 若删除失败,强制重新初始化临时文件(解决文件锁定问题) + if (!deleteSuccess) { + initCropTempFileAgain(); + } + } + + // 核心修复1:校验并修复裁剪临时文件权限(Android14+ 必需,避免创建失败) + if (_mSourceCropTempFile == null) { + LogUtils.d(TAG, "【裁剪准备】裁剪临时文件为空,重新初始化"); + initCropTempFileAgain(); // 重新初始化临时文件 + } + // 校验临时文件目录权限,无权限则切换到内部存储目录(终极兜底) + if (!_mSourceCropTempFile.getParentFile().canWrite()) { + LogUtils.d(TAG, "【裁剪准备】裁剪目录无权限,切换到内部存储目录"); + initCropTempFileAgain(true); // 强制使用内部存储目录初始化 + } + + // 创建新裁剪临时文件并强制设置权限(Android14+ 必需) _mSourceCropTempFile.createNewFile(); - // 核心优化:设置文件权限(确保裁剪工具可读写,适配缓存目录权限) - _mSourceCropTempFile.setReadable(true, false); - _mSourceCropTempFile.setWritable(true, false); - _mSourceCropTempFile.setExecutable(false, false); // 关闭执行权限,提升安全性 - LogUtils.d(TAG, "【裁剪准备】新裁剪临时文件(缓存目录)创建成功,路径:" + _mSourceCropTempFile.getAbsolutePath() + ",读写权限:" + _mSourceCropTempFile.canRead() + "/" + _mSourceCropTempFile.canWrite()); + _mSourceCropTempFile.setReadable(true, true); // 允许裁剪工具读取(所有用户) + _mSourceCropTempFile.setWritable(true, true); // 允许裁剪工具写入(所有用户) + _mSourceCropTempFile.setExecutable(false, true); // 关闭执行权限,提升安全性 + LogUtils.d(TAG, "【裁剪准备】新裁剪临时文件创建成功:" + + "路径=" + _mSourceCropTempFile.getAbsolutePath() + + ",可写=" + _mSourceCropTempFile.canWrite() + + ",可读=" + _mSourceCropTempFile.canRead() + + ",目录类型=" + (_mSourceCropTempFile.getParentFile().getAbsolutePath().contains(getFilesDir().getAbsolutePath()) ? "内部存储目录" : "应用私有/缓存目录")); } catch (IOException e) { - LogUtils.d(TAG, "【裁剪异常】缓存目录临时文件创建失败:" + e.getMessage(), Thread.currentThread().getStackTrace()); - ToastUtils.show("剪裁临时文件创建失败"); + LogUtils.e(TAG, "【裁剪异常】临时文件创建失败(Permission denied):" + e.getMessage(), e); + ToastUtils.show("裁剪临时文件创建失败,请重试"); + // 异常时清理并重新初始化临时文件,为下次操作做准备 + clearCropTempFile(); + initCropTempFileAgain(true); return; } - // 生成裁剪输出Uri(适配缓存目录文件) - try { - cropOutPutUri = getUriForFile(this, _mSourceCropTempFile); - LogUtils.d(TAG, "【裁剪Uri】裁剪输出Uri(缓存目录)生成成功 : " + cropOutPutUri.toString()); - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】生成裁剪输出Uri失败:" + e.getMessage()); - ToastUtils.show("图片裁剪失败:无法创建临时文件"); - return; - } - - // 核心修复2:裁剪意图兼容(适配不同机型,移除固定包名限制) + // 第四步:构建裁剪意图(适配MIUI裁剪工具+Android14+ 权限) Intent intent = new Intent("com.android.camera.action.CROP"); - // 兼容部分机型不支持隐式意图,添加多包名适配(可选,按需启用) - // intent.setPackage("com.android.camera"); // 已注释,避免限制过严 - intent.setDataAndType(inputUri, "image/" + _mszCommonFileType); - intent.putExtra("crop", "true"); - intent.putExtra("noFaceDetection", true); + intent.setDataAndType(inputUri, "image/*"); // 简化图片类型,适配更多裁剪工具 + intent.putExtra("crop", "true"); // 启用裁剪功能(必传) + intent.putExtra("noFaceDetection", true); // 关闭人脸识别,提升兼容性 + intent.putExtra("circleCrop", false); // 关闭圆形裁剪(适配矩形裁剪场景) - // 裁剪比例逻辑(固定比例时计算,自由裁剪时不设置) + // 第五步:设置裁剪比例(固定/自由裁剪适配) if (!isCropFree) { int viewWidth = bvPreviewBackground.getWidth(); int viewHeight = bvPreviewBackground.getHeight(); - // 控件未测量完成时,使用屏幕尺寸作为比例 + // 控件未测量完成时,使用屏幕尺寸作为比例(避免比例为0) if (viewWidth <= 0 || viewHeight <= 0) { viewWidth = getResources().getDisplayMetrics().widthPixels; viewHeight = getResources().getDisplayMetrics().heightPixels; LogUtils.d(TAG, "【裁剪比例】控件未测量完成,使用屏幕尺寸计算比例:" + viewWidth + "x" + viewHeight); } - - // 计算最大公约数,简化宽高比 + // 计算最大公约数,简化宽高比(避免比例过大导致裁剪工具异常) int gcd = calculateGCD(viewWidth, viewHeight); int simplifiedWidth = viewWidth / gcd; int simplifiedHeight = viewHeight / gcd; - - intent.putExtra("aspectX", simplifiedWidth); - intent.putExtra("aspectY", simplifiedHeight); + intent.putExtra("aspectX", simplifiedWidth); // 裁剪宽比例 + intent.putExtra("aspectY", simplifiedHeight); // 裁剪高比例 LogUtils.d(TAG, "【裁剪比例】原始比例:" + viewWidth + ":" + viewHeight + ",简化后比例:" + simplifiedWidth + ":" + simplifiedHeight); + } else { + // 自由裁剪:设置默认比例1:1(兼容部分裁剪工具不支持无比例配置) + intent.putExtra("aspectX", 1); + intent.putExtra("aspectY", 1); + LogUtils.d(TAG, "【裁剪比例】自由裁剪,默认比例1:1(可手动调整)"); } - // 裁剪参数配置 - intent.putExtra("return-data", false); // 不返回Bitmap,避免OOM - intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); // 输出到缓存目录临时文件 - intent.putExtra("scale", true); // 允许缩放 - intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); // 输出格式 - // 授予裁剪工具读写权限(关键,避免缓存目录权限不足) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + // 第六步:设置裁剪输出尺寸(适配MIUI限制+Android14+ 内存优化) + int screenWidth = getResources().getDisplayMetrics().widthPixels; + int screenHeight = getResources().getDisplayMetrics().heightPixels; + int maxOutputSize = 2048; // MIUI裁剪工具兼容最大尺寸(避免0字节文件) + // 适配Android14+ 内存,进一步限制尺寸(避免大图片OOM) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + maxOutputSize = 1536; // Android14+ 最大尺寸限制为1536x1536(平衡清晰度和内存) + } + int outputX = Math.min(screenWidth, maxOutputSize); // 限制宽度≤最大尺寸 + int outputY = Math.min(screenHeight, maxOutputSize); // 限制高度≤最大尺寸 + intent.putExtra("outputX", outputX); // 裁剪后宽度(适配MIUI+Android14+) + intent.putExtra("outputY", outputY); // 裁剪后高度(适配MIUI+Android14+) + intent.putExtra("scale", true); // 允许缩放(必传,否则裁剪后图片模糊) + intent.putExtra("scaleUpIfNeeded", true); // 自动缩放(小图填满裁剪区域,避免空白) + LogUtils.d(TAG, "【裁剪适配】输出尺寸设置完成:" + + "outputX=" + outputX + + ",outputY=" + outputY + + ",最大限制=" + maxOutputSize + + "(适配MIUI裁剪+Android14+ 内存)"); - // 核心修复3:添加意图启动校验(避免启动失败无响应) + // 第七步:明确输出格式和质量(避免格式不兼容导致写入失败) + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); // 固定JPEG格式,兼容性最好 + intent.putExtra("quality", 80); // 裁剪输出质量80%(平衡清晰度和文件大小) + + // 第八步:核心配置(禁用返回Bitmap,避免OOM,输出到临时文件) + intent.putExtra("return-data", false); // 禁用返回Bitmap,必传!(Android14+ 大图片必设) + // 注:此处先不设置EXTRA_OUTPUT,后续生成适配MIUI的outputUri后再设置 + + // 第九步:添加权限Flags(Android14+ 及MIUI裁剪工具必需) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予读取权限(读取输入图片) + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 授予写入权限(写入输出临时文件) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 适配部分机型裁剪后无回调问题 + // Android14+ 新增:添加FLAG_GRANT_PERSISTABLE_URI_PERMISSION,确保权限持久化 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } + LogUtils.d(TAG, "【裁剪权限】意图权限Flags已添加(适配Android14+ 及MIUI)"); + + // 第十步:适配Android14+ 及MIUI裁剪工具兼容性(获取可用裁剪工具,避免启动无效工具) try { - startActivityForResult(intent, REQUEST_CROP_IMAGE); - LogUtils.d(TAG, "【裁剪启动】裁剪意图已启动,请求码:" + REQUEST_CROP_IMAGE + ",目标输出Uri(缓存目录):" + cropOutPutUri.toString()); - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】启动裁剪窗口失败:" + e.getMessage()); - ToastUtils.show("无法启动裁剪工具,请安装系统相机"); - // 兼容方案:若系统相机不支持,使用第三方裁剪工具 - Intent chooserIntent = Intent.createChooser(intent, "选择裁剪工具"); - if (chooserIntent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(chooserIntent, REQUEST_CROP_IMAGE); - LogUtils.d(TAG, "【裁剪兼容】已启动第三方裁剪工具选择器"); + List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (!resolveInfos.isEmpty()) { + // 选择第一个可用的裁剪工具(优先系统相机裁剪,适配MIUI) + ResolveInfo resolveInfo = resolveInfos.get(0); + String cropPackageName = resolveInfo.activityInfo.packageName; + String cropActivityName = resolveInfo.activityInfo.name; + LogUtils.d(TAG, "【裁剪适配】找到可用裁剪工具:" + + "包名=" + cropPackageName + + ",Activity=" + cropActivityName); + + // 核心修复2:生成输出Uri时,显式传入裁剪工具包名(授予精准权限,解决MIUI权限问题) + cropOutPutUri = getUriForFile(this, _mSourceCropTempFile, cropPackageName); // 关键:传入cropPackageName + LogUtils.d(TAG, "【裁剪Uri】裁剪输出Uri(适配MIUI)生成成功 : " + cropOutPutUri.toString()); + + // 核心修复3:显式设置ComponentName后,清空原意图的data(解决MIUI参数冲突,避免写入失败) + Intent cropIntent = new Intent(intent); + cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); + cropIntent.setDataAndType(null, null); // 关键:清空原data,避免MIUI参数解析冲突 + cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); // 重新设置输出Uri(确保生效) + // 重新添加权限(避免设置Component后权限丢失) + cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + cropIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } + + // 启动裁剪工具(显式指定Activity,避免启动错误工具) + startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); + LogUtils.d(TAG, "【裁剪启动】裁剪意图已启动(适配Android14+ 及MIUI):" + + "请求码=" + REQUEST_CROP_IMAGE + + ",输出Uri=" + cropOutPutUri.toString() + + ",裁剪工具包名=" + cropPackageName); } else { - LogUtils.e(TAG, "【裁剪失败】无任何裁剪工具可用,建议安装系统相机"); - ToastUtils.show("无可用裁剪工具"); + // 无系统裁剪工具,启动第三方裁剪工具选择器(兜底兼容) + cropOutPutUri = getUriForFile(this, _mSourceCropTempFile); // 生成通用输出Uri + intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); // 补充输出Uri + Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + chooser.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } + if (chooser.resolveActivity(getPackageManager()) != null) { + startActivityForResult(chooser, REQUEST_CROP_IMAGE); + LogUtils.d(TAG, "【裁剪兼容】已启动第三方裁剪工具选择器(适配无系统裁剪工具场景)"); + } else { + LogUtils.e(TAG, "【裁剪失败】无任何裁剪工具可用,建议安装系统相机"); + ToastUtils.show("无可用裁剪工具,请安装系统相机"); + // 清理临时文件,避免占用空间 + clearCropTempFile(); + } + } + } catch (Exception e) { + LogUtils.e(TAG, "【裁剪异常】启动裁剪窗口失败:" + e.getMessage(), e); + ToastUtils.show("无法启动裁剪工具,请重试"); + // 异常时清理临时文件,为下次操作做准备 + clearCropTempFile(); + } + } + + /** + * 重新初始化裁剪临时文件(适配Java7 + Android全版本,解决Permission denied+文件锁定问题) + * 核心适配:移除Java8+特性(Lambda、可变参数简化写法),兼容Java7语法,保留Android14+权限兜底逻辑 + * @param forceInternalDir 是否强制使用内部存储目录(true=强制使用,权限最稳定) + * 注:Java7可变参数需显式处理长度,避免空指针 + */ + private void initCropTempFileAgain(boolean... forceInternalDir) { + // 1. 处理可变参数(Java7兼容:显式判断参数长度,避免空指针) + boolean force = false; + if (forceInternalDir != null && forceInternalDir.length > 0) { + force = forceInternalDir[0]; + } + File cropBaseDir = null; + + // 2. 选择裁剪基础目录(Java7 if-else判断,避免Lambda) + // 强制使用内部存储目录(Android14+ 权限最稳定,优先选择) + if (force) { + cropBaseDir = getFilesDir(); + LogUtils.d(TAG, "【临时文件重构】强制使用内部存储目录初始化裁剪临时文件"); + } else { + // 优先使用应用私有外部目录,不可用则切换到内部存储目录(Java7非空校验) + File externalDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (externalDir != null) { + cropBaseDir = externalDir; + } else { + cropBaseDir = getFilesDir(); + LogUtils.d(TAG, "【临时文件重构】应用私有外部目录不可用,切换到内部存储目录"); + } + } + + // 3. 创建裁剪临时子目录并设置权限(Java7兼容,调用Java7版setDirPermissionsRecursively) + File cropTempDir = new File(cropBaseDir, "CropTemp"); + if (!cropTempDir.exists()) { + cropTempDir.mkdirs(); + // 调用适配Java7的递归权限设置函数(确保目录权限生效) + setDirPermissionsRecursively(cropTempDir); + // Java7字符串拼接,避免String.join + String dirLog = "【临时文件重构】创建裁剪临时目录:" + cropTempDir.getAbsolutePath() + + ",目录权限:可写=" + cropTempDir.canWrite(); + LogUtils.d(TAG, dirLog); + } + + // 4. 重新初始化裁剪临时文件(先删后建,Java7兼容写法) + _mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName); + try { + // 先删除旧文件(避免权限残留/文件锁定,Java7显式判断) + if (_mSourceCropTempFile.exists()) { + boolean deleteSuccess = _mSourceCropTempFile.delete(); + String deleteLog = "【临时文件重构】删除旧临时文件:" + (deleteSuccess ? "成功" : "失败"); + LogUtils.d(TAG, deleteLog); + } + // 重新创建文件并强制设置权限(Java7兼容,第二个参数设为false=允许所有用户访问) + _mSourceCropTempFile.createNewFile(); + _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setWritable(true, false); + _mSourceCropTempFile.setExecutable(false, false); + + // 打印初始化结果(Java7变量提前定义,避免日志拼接中多次调用方法) + String filePath = _mSourceCropTempFile.getAbsolutePath(); + boolean canWrite = _mSourceCropTempFile.canWrite(); + boolean canRead = _mSourceCropTempFile.canRead(); + String dirType = ""; + if (cropBaseDir.equals(getFilesDir())) { + dirType = "内部存储目录"; + } else { + dirType = "应用私有外部目录"; + } + String initLog = "【临时文件重构】裁剪临时文件重新初始化成功:路径=" + filePath + + ",可写=" + canWrite + ",可读=" + canRead + ",目录类型=" + dirType; + LogUtils.d(TAG, initLog); + + } catch (IOException e) { + // 5. 异常处理:应用私有/内部目录失败,切换到缓存目录兜底(Java7兼容) + String errLog = "【临时文件重构】初始化失败:" + e.getMessage(); + LogUtils.e(TAG, errLog, e); + + // 终极兜底:使用应用缓存目录(getCacheDir()),权限最宽松(Java7非空校验) + File cacheCropDir = new File(getCacheDir(), "CropTemp"); + if (!cacheCropDir.exists()) { + cacheCropDir.mkdirs(); + setDirPermissionsRecursively(cacheCropDir); + } + _mSourceCropTempFile = new File(cacheCropDir, _mSourceCropTempFileName); + + try { + // 缓存目录下先删后建(Java7兼容) + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + _mSourceCropTempFile.createNewFile(); + _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setWritable(true, false); + + String cacheLog = "【临时文件重构】缓存目录兜底初始化成功:" + _mSourceCropTempFile.getAbsolutePath(); + LogUtils.d(TAG, cacheLog); + + } catch (IOException ex) { + // 极端场景:缓存目录也失败,提示用户重启应用(Java7用匿名内部类Runnable) + String finalErrLog = "【临时文件重构】缓存目录兜底失败:" + ex.getMessage(); + LogUtils.e(TAG, finalErrLog, ex); + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("裁剪文件初始化失败,请重启应用重试"); + } + }); } } } @@ -632,63 +1062,131 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 工具方法:生成Content Uri(适配Android 7.0+,多包名兼容,适配缓存目录文件) + * 工具方法:生成Content Uri(适配Android 7.0+,多包名兼容,重点适配MIUI裁剪工具显式权限) + * @param context 上下文 + * @param file 目标文件 + * @param targetPackageName 目标应用包名(传入裁剪工具包名,显式授予权限,适配MIUI) + * @return 生成的Content Uri + * @throws Exception 生成失败时抛出异常 */ - private Uri getUriForFile(Context context, File file) throws Exception { + private Uri getUriForFile(Context context, File file, String targetPackageName) throws Exception { + // 核心修复:在函数顶部定义appPrivateDir(全局可用,解决API<24分支未定义问题) + String appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) != null + ? getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + : getFilesDir().getAbsolutePath(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { - //.N 核心:使用BuildConfig.APPLICATION_ID + ".fileprovider",与Manifest一致,适配多包名 + // 核心:使用BuildConfig.APPLICATION_ID + ".fileprovider",与AndroidManifest.xml配置一致,适配多包名 + String authority = BuildConfig.APPLICATION_ID + ".fileprovider"; Uri uri = FileProvider.getUriForFile( context, - BuildConfig.APPLICATION_ID + ".fileprovider", + authority, file ); - // 增强:授予所有应用临时读写权限(适配缓存目录文件+第三方裁剪工具/相机) - context.grantUriPermission( - "*", // 临时授权所有应用,退出后失效,安全性无影响 - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ); - LogUtils.d(TAG, "【FileProvider】Uri生成成功:包名=" + BuildConfig.APPLICATION_ID + ",文件路径=" + file.getPath() + "(是否缓存目录:" + file.getAbsolutePath().contains(getCacheDir().getAbsolutePath()) + "),Uri=" + uri.toString()); + // 核心修复:适配MIUI,显式授予目标裁剪工具权限(拒绝通配符“*”) + if (!TextUtils.isEmpty(targetPackageName)) { + // 仅授予当前裁剪工具(如com.miui.gallery)读写权限,MIUI可识别 + context.grantUriPermission( + targetPackageName, + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + LogUtils.d(TAG, "【FileProvider】已显式授予裁剪工具[" + targetPackageName + "]读写权限(适配MIUI)"); + } else { + // 兜底:无目标包名时授予所有应用权限(兼容非MIUI机型) + context.grantUriPermission( + "*", + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + LogUtils.d(TAG, "【FileProvider】已授予所有应用临时权限(兜底兼容)"); + } + + // 日志优化:精准打印路径类型、FileProvider别名(与res/xml/file_paths.xml对应,便于调试) + String filePath = file.getAbsolutePath(); + String backgroundSourceDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()).getAbsolutePath(); + String pathType = ""; + String pathAlias = ""; + + // 区分文件路径类型及对应FileProvider别名(与file_paths.xml严格对应) + if (filePath.contains(appPrivateDir)) { + pathType = "应用私有外部目录(Pictures)"; + pathAlias = "app_private_pictures"; // 与res/xml/file_paths.xml中配置的别名一致 + } else if (filePath.contains(backgroundSourceDir)) { + pathType = "背景图片目录(BackgroundSource)"; + pathAlias = "background_source"; // 与res/xml/file_paths.xml中配置的别名一致 + } else if (filePath.contains(getCacheDir().getAbsolutePath())) { + pathType = "应用内部缓存目录"; + pathAlias = "cache_path"; // 与res/xml/file_paths.xml中配置的别名一致 + } else { + pathType = "外部存储目录"; + pathAlias = "未知(需检查file_paths.xml配置)"; + } + + // 详细日志:打印关键信息,便于排查Uri路径匹配/别名不匹配问题 + LogUtils.d(TAG, "【FileProvider】Uri生成成功:" + + "包名=" + BuildConfig.APPLICATION_ID + + ",Authority=" + authority + + ",文件路径=" + file.getPath() + + ",路径类型=" + pathType + + ",FileProvider别名=" + pathAlias + + ",目标授权包名=" + (targetPackageName != null ? targetPackageName : "无") + + ",最终Uri=" + uri.toString()); return uri; } catch (Exception e) { - // 打印详细错误信息,便于多包名/FileProvider/缓存目录权限问题排查 - String errMsg = "FileProvider生成Uri失败:包名=" + BuildConfig.APPLICATION_ID - + ",文件路径=" + file.getPath() + "(是否缓存目录:" + file.getAbsolutePath().contains(getCacheDir().getAbsolutePath()) + "),错误信息:" + e.getMessage(); - LogUtils.e(TAG, errMsg); - throw new Exception(errMsg); // 抛出异常,让上层处理 + // 异常日志强化:打印完整错误信息,包括包名、文件路径、别名匹配情况,快速定位问题 + String errMsg = "FileProvider生成Uri失败:" + + "包名=" + BuildConfig.APPLICATION_ID + + ",Authority=" + (BuildConfig.APPLICATION_ID + ".fileprovider") + + ",文件路径=" + file.getPath() + + ",目标授权包名=" + (targetPackageName != null ? targetPackageName : "无") + + ",错误信息:" + e.getMessage(); + LogUtils.e(TAG, errMsg, e); + // 抛出异常让上层处理(避免静默失败,便于定位问题) + throw new Exception(errMsg); } } else { - // Android 7.0以下,直接使用Uri.fromFile(兼容旧机型,缓存目录文件同样支持) + // Android 7.0以下,直接使用Uri.fromFile(兼容旧机型,应用私有外部目录同样支持) Uri uri = Uri.fromFile(file); - LogUtils.d(TAG, "【兼容旧机型】Android7.0以下,直接生成Uri:" + uri.toString() + ",文件路径:" + file.getPath() + "(是否缓存目录:" + file.getAbsolutePath().contains(getCacheDir().getAbsolutePath()) + ")"); + // 日志打印:标记旧机型兼容模式(appPrivateDir已在函数顶部定义,此处可直接使用) + LogUtils.d(TAG, "【兼容旧机型】Android7.0以下(API<24),直接生成Uri:" + + "文件路径=" + file.getPath() + + ",Uri=" + uri.toString() + + ",路径类型=" + (file.getAbsolutePath().contains(appPrivateDir) ? "应用私有外部目录" : "其他目录")); return uri; } } + // 保留原无参数getUriForFile(兼容其他调用场景,避免编译报错) + private Uri getUriForFile(Context context, File file) throws Exception { + return getUriForFile(context, file, null); // 调用新方法,目标包名传null(兜底) + } + /** - * 保存剪裁后的Bitmap(彻底修复:路径由BackgroundSourceUtils统一管理,适配缓存目录临时文件) + * 保存剪裁后的Bitmap(终极修复:强化预览同步+双重刷新+权限适配,确保图片加载到bvPreviewBackground控件) + * 核心目标:将裁剪后的图片同步到预览Bean+直接设置到BackgroundView,双重保障显示 */ private void saveCropBitmap(Bitmap bitmap) { - LogUtils.d(TAG, "【保存启动】saveCropBitmap 触发,开始处理裁剪图片(临时文件位于缓存目录)"); + LogUtils.d(TAG, "【保存启动】saveCropBitmap 触发,开始处理裁剪图片(临时文件位于应用私有外部目录)"); if (bitmap == null || bitmap.isRecycled()) { - ToastUtils.show("裁剪图片为空,请重新裁剪"); // 优化吐司,简洁明确 + ToastUtils.show("裁剪图片为空,请重新裁剪"); LogUtils.e(TAG, "【保存失败】裁剪图片为空或已回收,无法保存"); - // 清理缓存目录下的临时文件 + // 清理应用私有外部目录下的裁剪临时文件(避免残留空文件) if (_mSourceCropTempFile.exists()) { boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【临时文件清理】裁剪图片无效,缓存目录临时文件清理:" + (deleteSuccess ? "成功" : "失败")); + LogUtils.d(TAG, "【临时文件清理】裁剪图片无效,应用私有外部目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); } return; } - // 内存优化:大图片自动缩放(超过10MB缩小,避免OOM) + // 内存优化:大图片自动缩放(超过10MB缩小,避免OOM,适配高清图片) Bitmap scaledBitmap = bitmap; int originalSize = bitmap.getByteCount() / 1024 / 1024; // 转换为MB LogUtils.d(TAG, "【图片缩放】原始Bitmap大小:" + originalSize + "MB,是否需要缩放:" + (originalSize > 10)); - if (originalSize > 10) { // 超过10MB + if (originalSize > 10) { // 超过10MB自动缩放 float scale = 1.0f; - while (scaledBitmap.getByteCount() / 1024 / 1024 > 5) { // 缩小到5MB以内 + while (scaledBitmap.getByteCount() / 1024 / 1024 > 5) { // 缩小到5MB以内(平衡清晰度和内存) scale -= 0.2f; // 每次缩小20% if (scale < 0.2f) break; // 最小缩放到20%,避免过度模糊 scaledBitmap = scaleBitmap(scaledBitmap, scale); @@ -696,23 +1194,26 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg int scaledSize = scaledBitmap.getByteCount() / 1024 / 1024; LogUtils.d(TAG, "【图片缩放】缩放完成:缩放比例=" + scale + ",缩放后大小=" + scaledSize + "MB"); if (scaledBitmap != bitmap) { - bitmap.recycle(); // 回收原Bitmap,释放内存 + bitmap.recycle(); // 回收原Bitmap,释放内存(避免内存泄漏) LogUtils.d(TAG, "【内存回收】原始Bitmap已回收"); } } - // 核心修复1:通过BackgroundSourceUtils获取统一的预览压缩图路径(替代未定义的fScaledCompressFilePath) + // 核心1:通过BackgroundSourceUtils获取统一的预览压缩图路径(确保与预览逻辑完全对齐,适配工具类路径管理) BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); - String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); // 统一路径:工具类拼接(背景目录+预览Bean压缩文件名) - File fScaledCompressBitmapFile = new File(scaledCompressFilePath); // 基于工具类路径创建文件对象 - LogUtils.d(TAG, "【保存准备】通过BackgroundSourceUtils获取统一保存路径:" + scaledCompressFilePath + ",裁剪临时文件(缓存目录):" + _mSourceCropTempFile.getAbsolutePath()); + String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); // 统一路径:由BackgroundSourceUtils管理,避免手动设置冲突 + File fScaledCompressBitmapFile = new File(scaledCompressFilePath); + LogUtils.d(TAG, "【保存准备】通过BackgroundSourceUtils获取统一保存路径:" + scaledCompressFilePath + ",裁剪临时文件路径:" + _mSourceCropTempFile.getAbsolutePath()); - // 确保保存目录存在(避免路径无效导致保存失败) + // 确保保存目录存在(避免路径无效导致保存失败,适配应用私有外部目录) File parentDir = fScaledCompressBitmapFile.getParentFile(); LogUtils.d(TAG, "【保存准备】目标保存目录:" + parentDir.getAbsolutePath() + ",是否存在:" + parentDir.exists()); if (!parentDir.exists()) { boolean mkdirSuccess = parentDir.mkdirs(); - LogUtils.d(TAG, "【保存准备】目录不存在,创建结果:" + (mkdirSuccess ? "成功" : "失败")); + // 强制设置目录权限(Android14+ 必须,否则保存失败) + parentDir.setWritable(true, false); + parentDir.setReadable(true, false); + LogUtils.d(TAG, "【保存准备】目录不存在,创建结果:" + (mkdirSuccess ? "成功" : "失败") + ",目录权限:可写=" + parentDir.canWrite()); if (!mkdirSuccess) { String errMsg = "无法创建保存目录:" + parentDir.getAbsolutePath(); LogUtils.e(TAG, "【保存失败】" + errMsg); @@ -722,7 +1223,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - // 优化:检查文件权限(确保可写,避免覆盖旧文件失败) + // 权限优化:检查文件权限(确保可写,避免覆盖旧文件失败,适配应用私有外部目录) if (fScaledCompressBitmapFile.exists()) { LogUtils.d(TAG, "【保存准备】目标文件已存在,检查是否可写:" + fScaledCompressBitmapFile.canWrite()); if (!fScaledCompressBitmapFile.canWrite()) { @@ -740,93 +1241,116 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg FileOutputStream fos = null; try { - // 修复2:强制设置文件可写权限(避免Android高版本权限限制) + // 强制设置文件可写权限(Android14+ 应用私有外部目录必须显式设置,否则写入失败) fScaledCompressBitmapFile.setWritable(true, false); - LogUtils.d(TAG, "【保存准备】目标文件权限设置:可写=" + fScaledCompressBitmapFile.canWrite()); + fScaledCompressBitmapFile.setReadable(true, false); + LogUtils.d(TAG, "【保存准备】目标文件权限设置完成:可写=" + fScaledCompressBitmapFile.canWrite() + ",可读=" + fScaledCompressBitmapFile.canRead()); fos = new FileOutputStream(fScaledCompressBitmapFile); - // 核心修复3:根据工具类路径中的文件名自动适配压缩格式(兼容JPEG/PNG) - String fileName = fScaledCompressBitmapFile.getName(); // 从工具类管理的路径中获取文件名 + + // 自动适配压缩格式(根据工具类路径中的文件名区分JPEG/PNG,避免格式错误导致图片损坏) + String fileName = fScaledCompressBitmapFile.getName(); Bitmap.CompressFormat compressFormat = fileName.endsWith(".png") - ? Bitmap.CompressFormat.PNG - : Bitmap.CompressFormat.JPEG; - LogUtils.d(TAG, "【保存配置】压缩格式:" + compressFormat + ",压缩质量:80%"); - // 压缩保存(80%质量,平衡清晰度和文件大小) + ? Bitmap.CompressFormat.PNG + : Bitmap.CompressFormat.JPEG; + LogUtils.d(TAG, "【保存配置】压缩格式:" + compressFormat + ",压缩质量:80%(平衡清晰度和文件大小)"); + + // 压缩保存(80%质量为最优选择,避免过度压缩导致模糊) boolean success = scaledBitmap.compress(compressFormat, 80, fos); - fos.flush(); - LogUtils.d(TAG, "【保存结果】图片压缩保存:" + (success ? "成功" : "失败") + ",目标路径:" + scaledCompressFilePath); + fos.flush(); // 强制写入数据,避免数据残留(解决部分机型保存后文件损坏问题) + fos.getFD().sync(); // 同步文件描述符,确保数据完全写入磁盘(Android14+ 关键) + LogUtils.d(TAG, "【保存结果】图片压缩保存:" + (success ? "成功" : "失败") + ",目标路径:" + scaledCompressFilePath + ",文件大小:" + (success ? fScaledCompressBitmapFile.length()/1024 + "KB" : "0")); if (success) { ToastUtils.show("图片保存成功"); BackgroundBean previewBean = utils.getPreviewBackgroundBean(); - // 核心修复4:仅同步工具类管理的压缩图完整路径(文件名由工具类维护,无需手动设置) - previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath); // 同步工具类路径,确保路径一致 - previewBean.setIsUseBackgroundFile(true); // 强制启用背景图,确保预览正常显示 - previewBean.setIsUseScaledCompress(true); // 启用压缩图,提升加载速度 - utils.saveSettings(); // 持久化配置到JSON文件 - LogUtils.d(TAG, "【Bean同步】预览Bean配置同步完成:压缩图路径=" + previewBean.getBackgroundScaledCompressFilePath() + ",是否启用背景=" + previewBean.isUseBackgroundFile()); + // 核心2:同步预览Bean配置(仅保留与BackgroundSourceUtils兼容的字段,删除手动设置路径的冲突代码) + previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath); // 压缩图路径(由工具类管理,无需额外设置原始路径) + previewBean.setIsUseBackgroundFile(true); // 强制启用背景图,确保预览加载 + previewBean.setIsUseScaledCompress(true); // 启用压缩图,提升预览加载速度 + utils.saveSettings(); // 持久化配置到JSON文件(由工具类统一管理,避免路径混乱) + LogUtils.d(TAG, "【Bean同步】预览Bean配置同步完成(适配BackgroundSourceUtils路径管理):压缩图路径=" + previewBean.getBackgroundScaledCompressFilePath() + ",是否启用背景=" + previewBean.isUseBackgroundFile()); - // 核心修复5:调用工具类方法同步文件(与BackgroundSourceUtils逻辑对齐,确保预览生效) + // 核心3:调用工具类同步文件(与BackgroundSourceUtils逻辑完全对齐,由工具类统一管理预览目录文件) utils.saveFileToPreviewBean(fScaledCompressBitmapFile, scaledCompressFilePath); - LogUtils.d(TAG, "【文件同步】已调用saveFileToPreviewBean,同步裁剪图到预览目录(路径由工具类统一管理)"); + LogUtils.d(TAG, "【文件同步】已调用saveFileToPreviewBean,由工具类同步裁剪图到预览目录(应用私有外部目录)"); - // 保存成功后清理缓存目录下的裁剪临时文件(避免占用缓存空间) - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【临时文件清理】裁剪成功,缓存目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); + // 核心4:直接设置图片路径到bvPreviewBackground(跳过Bean间接同步,确保实时显示) + // 适配BackgroundView:若有setPreviewBackgroundPath方法则直接调用,无则注释此段(需手动在BackgroundView新增) + if (bvPreviewBackground != null) { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【直接设置】已将裁剪图路径直接设置到bvPreviewBackground:" + scaledCompressFilePath); } - // 刷新预览视图(双重保障,确保裁剪图实时显示) + // 核心5:主线程双重刷新预览(确保图片加载到bvPreviewBackground,解决刷新不及时问题) runOnUiThread(new Runnable() { @Override public void run() { + // 第一次刷新:立即刷新视图 bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】保存成功后,主线程刷新预览视图"); + LogUtils.d(TAG, "【预览刷新】第一次刷新bvPreviewBackground(立即)"); + // 第二次刷新:延迟300ms(适配控件渲染耗时,避免视图未更新) + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【预览刷新】第二次刷新bvPreviewBackground(延迟300ms),确保图片正常显示"); + } + }, 300); } }); + // 保存成功后,清理应用私有外部目录下的裁剪临时文件(释放空间,避免残留) + if (_mSourceCropTempFile.exists()) { + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【临时文件清理】裁剪成功,应用私有外部目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); + } + } else { String errMsg = "图片压缩失败(Bitmap压缩异常)"; LogUtils.e(TAG, "【保存失败】" + errMsg); ToastUtils.show("保存时发生错误:" + errMsg); - // 保存失败,禁用压缩图,避免预览显示异常 + // 保存失败,回滚配置(避免预览显示异常,适配工具类路径管理) BackgroundBean previewBean = utils.getPreviewBackgroundBean(); previewBean.setIsUseScaledCompress(false); - utils.saveSettings(); // 回滚配置 - LogUtils.d(TAG, "【配置回滚】保存失败,预览Bean禁用压缩图"); - // 清理缓存目录下的临时文件 + previewBean.setIsUseBackgroundFile(false); + utils.saveSettings(); + LogUtils.d(TAG, "【配置回滚】保存失败,预览Bean禁用背景图(避免预览异常)"); + // 清理应用私有外部目录下的裁剪临时文件 if (_mSourceCropTempFile.exists()) { boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【临时文件清理】裁剪失败,缓存目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); + LogUtils.d(TAG, "【临时文件清理】裁剪失败,应用私有外部目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); + } + // 刷新预览,显示最新状态(避免残留旧图) + if (bvPreviewBackground != null) { + bvPreviewBackground.reloadPreviewBackground(); } - // 刷新预览,显示最新状态 - bvPreviewBackground.reloadPreviewBackground(); } } catch (FileNotFoundException e) { String errMsg = "文件未找到:" + e.getMessage() + ",保存路径:" + scaledCompressFilePath; LogUtils.e(TAG, "【保存异常-文件未找到】" + errMsg); ToastUtils.show("保存时发生错误:" + errMsg); - // 清理缓存目录下的临时文件 + // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } } catch (IOException e) { - String errMsg = "写入失败:" + e.getMessage() + "(可能是权限不足)"; + String errMsg = "写入失败:" + e.getMessage() + "(应用私有外部目录权限不足或文件损坏)"; LogUtils.e(TAG, "【保存异常-IO错误】" + errMsg); ToastUtils.show("保存时发生错误:" + errMsg); - // 清理缓存目录下的临时文件 + // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } - } catch (Exception e) { // 捕获所有异常,避免崩溃和遗漏 + } catch (Exception e) { // 捕获所有异常,避免应用崩溃 String errMsg = "未知错误:" + e.getMessage(); LogUtils.e(TAG, "【保存异常-未知错误】" + errMsg, e); ToastUtils.show("保存时发生错误:" + errMsg); - // 清理缓存目录下的临时文件 + // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } } finally { - // 关闭流资源(避免资源泄漏) + // 关闭流资源(避免资源泄漏,适配大图片场景) if (fos != null) { try { fos.close(); @@ -835,31 +1359,33 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.e(TAG, "【资源清理异常】流关闭失败:" + e.getMessage()); } } - // 回收Bitmap(避免内存泄漏,尤其是大图片) + // 回收缩放后的Bitmap(避免内存泄漏,尤其是高清图片裁剪后) if (scaledBitmap != null && !scaledBitmap.isRecycled()) { scaledBitmap.recycle(); - LogUtils.d(TAG, "【资源清理】缩放后的Bitmap已回收"); + LogUtils.d(TAG, "【资源清理】缩放后的Bitmap已回收(释放内存)"); } - LogUtils.d(TAG, "【保存流程】saveCropBitmap 流程结束"); + LogUtils.d(TAG, "【保存流程】saveCropBitmap 流程结束(适配BackgroundSourceUtils路径管理)"); } } /** - * 缩放Bitmap(按比例缩小,避免OOM) + * 辅助方法:缩放Bitmap(适配大图片压缩,避免OOM) + * @param bitmap 原始Bitmap + * @param scale 缩放比例(0.1~1.0) + * @return 缩放后的Bitmap */ - private Bitmap scaleBitmap(Bitmap original, float scale) { - LogUtils.d(TAG, "【Bitmap缩放】scaleBitmap 触发,缩放比例:" + scale); - if (original == null || original.isRecycled()) { - LogUtils.e(TAG, "【Bitmap缩放失败】原始Bitmap为空或已回收"); - return null; + private Bitmap scaleBitmap(Bitmap bitmap, float scale) { + if (bitmap == null || scale <= 0 || scale >= 1.0f) { + return bitmap; } - int width = (int) (original.getWidth() * scale); - int height = (int) (original.getHeight() * scale); - // 确保宽高为正(避免缩放比例过小导致宽高为0) - width = Math.max(width, 1); - height = Math.max(height, 1); - LogUtils.d(TAG, "【Bitmap缩放】原始宽高:" + original.getWidth() + "x" + original.getHeight() + ",缩放后宽高:" + width + "x" + height); - return Bitmap.createScaledBitmap(original, width, height, true); // true:开启抗锯齿,提升清晰度 + int width = Math.round(bitmap.getWidth() * scale); + int height = Math.round(bitmap.getHeight() * scale); + // 使用Matrix缩放,保证图片清晰度 + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + Bitmap scaledBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + LogUtils.d(TAG, "【Bitmap缩放】缩放前:" + bitmap.getWidth() + "x" + bitmap.getHeight() + ",缩放后:" + width + "x" + height); + return scaledBitmap; } /** @@ -915,218 +1441,330 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg super.onActivityResult(requestCode, resultCode, data); LogUtils.d(TAG, "【回调触发】onActivityResult 触发,requestCode:" + requestCode + ",resultCode:" + resultCode + "(RESULT_OK=" + RESULT_OK + ")"); - // 处理图片选择回调(REQUEST_SELECT_PICTURE) - if (requestCode == REQUEST_SELECT_PICTURE && resultCode == RESULT_OK) { - try { - Uri selectedImage = data.getData(); - if (selectedImage == null) { - ToastUtils.show("选择的图片Uri为空"); - LogUtils.e(TAG, "【选图回调失败】选择的图片Uri为空,无法解析"); - return; - } - LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString()); - - // 核心修复:对ACTION_GET_CONTENT返回的Uri,添加持久化权限(Android 4.4+,避免后续访问无权限) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - getContentResolver().takePersistableUriPermission( - selectedImage, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ); - LogUtils.d(TAG, "【选图权限】已为选择的图片Uri添加持久化读取权限"); - } - - // 路径解析逻辑(适配不同Uri格式,避免解析失败) - File fSrcImage = null; - String filePath = UriUtil.getFilePathFromUri(this, selectedImage); - LogUtils.d(TAG, "【选图解析】Uri解析后的文件路径:" + filePath); - if (!TextUtils.isEmpty(filePath)) { - fSrcImage = new File(filePath); - } else { - // Uri解析失败,通过流复制生成临时文件(兜底方案,生成到应用缓存目录) - File cacheTempDir = new File(getCacheDir(), "SelectTemp"); - if (!cacheTempDir.exists()) { - cacheTempDir.mkdirs(); - LogUtils.d(TAG, "【选图解析】创建缓存目录临时子目录:" + cacheTempDir.getAbsolutePath()); - } - fSrcImage = new File(cacheTempDir, "selected_temp.jpg"); - if (fSrcImage.exists()) { - fSrcImage.delete(); - LogUtils.d(TAG, "【选图解析】旧缓存临时文件已清理,准备生成新临时文件"); - } - // 流复制生成临时文件(适配ContentProvider Uri,无需外部存储权限) - FileUtils.copyStreamToFile(getContentResolver().openInputStream(selectedImage), fSrcImage); - LogUtils.d(TAG, "【选图解析】Uri解析失败,通过流复制生成缓存临时文件:" + fSrcImage.getPath()); - } - - // 校验解析后的图片文件有效性 - LogUtils.d(TAG, "【选图校验】解析后图片文件:路径=" + (fSrcImage != null ? fSrcImage.getAbsolutePath() : "null") + ",是否存在=" + (fSrcImage != null && fSrcImage.exists()) + ",文件大小=" + (fSrcImage != null ? fSrcImage.length() + " bytes" : "0")); - if (fSrcImage == null || !fSrcImage.exists() || fSrcImage.length() <= 0) { - ToastUtils.show("选择的图片文件不存在或损坏"); - LogUtils.e(TAG, "【选图回调失败】解析后的图片文件无效"); - return; - } - - // 同步图片到预览Bean并刷新预览 - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); - utils.saveFileToPreviewBean(fSrcImage, selectedImage.toString()); - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【选图完成】预览图片已更新,启动裁剪(固定比例)"); - // 启动固定比例裁剪 - startCropImageActivity(false); - } catch (Exception e) { - String errMsg = "选择图片异常:" + e.getMessage(); - LogUtils.e(TAG, errMsg, e); - ToastUtils.show("选择图片失败:" + errMsg.substring(0, 20)); - // 异常时清理缓存目录下的裁剪临时文件 - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【选图异常清理】缓存目录裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getPath()); - } - } - } - // 处理拍照回调(REQUEST_TAKE_PHOTO) - else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) { - LogUtils.d(TAG, "【拍照回调】REQUEST_TAKE_PHOTO 回调触发,开始处理拍照结果"); - // 检查拍照文件是否有效(核心校验,避免空文件) - LogUtils.d(TAG, "【拍照校验】拍照文件路径:" + mfTakePhoto.getAbsolutePath() + ",是否存在:" + mfTakePhoto.exists() + ",文件大小:" + mfTakePhoto.length() + " bytes"); - if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { - ToastUtils.show("拍照文件不存在或损坏"); - LogUtils.e(TAG, "【拍照回调失败】拍照文件无效,可能是相机未正常保存"); - // 清理缓存目录下的裁剪临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【拍照异常清理】拍照文件无效,清理缓存目录裁剪临时文件"); - } - return; + try { + // 事件分发:根据requestCode调用对应功能函数 + if (requestCode == REQUEST_SELECT_PICTURE) { + handleSelectPictureResult(resultCode, data); // 处理选图回调 + } else if (requestCode == REQUEST_TAKE_PHOTO) { + handleTakePhotoResult(resultCode, data); // 处理拍照回调 + } else if (requestCode == REQUEST_CROP_IMAGE) { + handleCropImageResult(resultCode, data); // 处理裁剪回调 + } else if (resultCode != RESULT_OK) { + handleOperationCancelOrFail(); // 处理所有取消/失败场景 } + } catch (Exception e) { + // 全局异常兜底:避免单一事件异常导致整个回调崩溃 + String errMsg = "onActivityResult 全局异常:" + e.getMessage(); + LogUtils.e(TAG, errMsg, e); + ToastUtils.show("操作失败:" + errMsg.substring(0, 20)); + // 异常时强制清理裁剪临时文件 + clearCropTempFile(); + } + } - // 获取拍照Bitmap并处理 - Bundle extras = data.getExtras(); - LogUtils.d(TAG, "【拍照回调】拍照数据Bundle:" + (extras != null ? "非空" : "为空")); - if (extras != null) { - Bitmap imageBitmap = (Bitmap) extras.get("data"); - LogUtils.d(TAG, "【拍照回调】获取拍照Bitmap:" + (imageBitmap != null ? "成功" : "失败") + ",是否回收:" + (imageBitmap != null && imageBitmap.isRecycled())); - if (imageBitmap != null && !imageBitmap.isRecycled()) { - // 压缩图片并保存 - compressQualityToRecivedPicture(imageBitmap); - LogUtils.d(TAG, "【拍照完成】拍照图片压缩完成,刷新预览并启动裁剪"); - // 拍照压缩后刷新预览 - bvPreviewBackground.reloadPreviewBackground(); - // 启动固定比例裁剪 - startCropImageActivity(false); - } else { - ToastUtils.show("拍照图片为空"); - LogUtils.e(TAG, "【拍照回调失败】拍照Bitmap为空或已回收"); - // 清理缓存目录下的裁剪临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【拍照异常清理】拍照图片为空,清理缓存目录裁剪临时文件:" + _mSourceCropTempFile.getPath()); - } - } + /** + * 处理图片选择回调(REQUEST_SELECT_PICTURE) + * 职责:解析选图Uri→生成临时文件→同步预览→启动裁剪 + */ + private void handleSelectPictureResult(int resultCode, Intent data) { + // 1. 校验结果合法性 + if (resultCode != RESULT_OK || data == null) { + handleOperationCancelOrFail(); + return; + } + + // 2. 解析选图Uri并添加持久化权限 + Uri selectedImage = data.getData(); + if (selectedImage == null) { + ToastUtils.show("选择的图片Uri为空"); + LogUtils.e(TAG, "【选图回调失败】选择的图片Uri为空,无法解析"); + clearCropTempFile(); + return; + } + LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString()); + + // 3. 适配Android4.4+,添加Uri持久化读取权限(避免后续访问无权限) + grantPersistableUriPermission(selectedImage); + + // 4. 解析Uri为文件(适配ContentProvider Uri,兜底流复制) + File selectedFile = parseUriToFile(selectedImage); + if (selectedFile == null || !selectedFile.exists() || selectedFile.length() <= 0) { + ToastUtils.show("选择的图片文件不存在或损坏"); + LogUtils.e(TAG, "【选图回调失败】解析后的图片文件无效"); + clearCropTempFile(); + return; + } + + // 5. 同步图片到预览并启动固定比例裁剪 + syncSelectedFileToPreview(selectedFile, selectedImage.toString()); + startCropImageActivity(false); + LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动固定比例裁剪"); + } + + /** + * 处理拍照回调(REQUEST_TAKE_PHOTO) + * 职责:校验拍照文件→获取Bitmap→压缩保存→同步预览→启动裁剪 + */ + private void handleTakePhotoResult(int resultCode, Intent data) { + // 1. 校验结果合法性 + if (resultCode != RESULT_OK || data == null) { + handleOperationCancelOrFail(); + return; + } + + // 2. 校验拍照文件有效性(核心:避免空文件/损坏文件) + if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { + ToastUtils.show("拍照文件不存在或损坏"); + LogUtils.e(TAG, "【拍照回调失败】拍照文件无效,路径:" + mfTakePhoto.getAbsolutePath()); + clearCropTempFile(); + return; + } + LogUtils.d(TAG, "【拍照校验】拍照文件有效,路径:" + mfTakePhoto.getAbsolutePath() + ",大小:" + mfTakePhoto.length() + " bytes"); + + // 3. 获取拍照Bitmap并压缩保存 + Bitmap photoBitmap = getTakePhotoBitmap(data); + if (photoBitmap == null || photoBitmap.isRecycled()) { + ToastUtils.show("拍照图片为空"); + LogUtils.e(TAG, "【拍照回调失败】拍照Bitmap为空或已回收"); + clearCropTempFile(); + return; + } + compressQualityToRecivedPicture(photoBitmap); // 复用原有压缩方法 + + // 4. 刷新预览并启动固定比例裁剪 + bvPreviewBackground.reloadPreviewBackground(); + startCropImageActivity(false); + LogUtils.d(TAG, "【拍照完成】拍照回调处理结束,已启动固定比例裁剪"); + } + + /** + * 处理裁剪回调(REQUEST_CROP_IMAGE) + * 职责:MIUI 0字节文件校验→解析裁剪Bitmap→保存图片→双重刷新预览 + */ + private void handleCropImageResult(int resultCode, Intent data) { + // 1. 裁剪文件基础校验(适配MIUI 0字节场景) + boolean isFileExist = _mSourceCropTempFile.exists(); + boolean isFileReadable = isFileExist ? _mSourceCropTempFile.canRead() : false; + long fileSize = isFileExist ? _mSourceCropTempFile.length() : 0; + boolean isFileValid = isFileExist && isFileReadable && fileSize > 100; + boolean isCropSuccess = (resultCode == RESULT_OK) || isFileValid; + + LogUtils.d(TAG, "【裁剪回调】容错校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + ",文件可读=" + isFileReadable + ",文件大小=" + fileSize + " bytes,是否判定为成功=" + isCropSuccess); + + // 2. 处理MIUI特有:文件存在但大小为0字节(裁剪工具写入失败) + if (isFileExist && fileSize == 0) { + handleMiuiCropZeroByteFile(); + return; + } + + // 3. 处理裁剪成功场景 + if (isCropSuccess) { + // 解析裁剪临时文件为Bitmap(适配大图片OOM) + Bitmap cropBitmap = parseCropTempFileToBitmap(); + if (cropBitmap != null && !cropBitmap.isRecycled()) { + // 保存裁剪图片并双重刷新预览 + saveCropBitmap(cropBitmap); + doubleRefreshPreview(); + LogUtils.d(TAG, "【裁剪完成】裁剪回调处理结束,图片已保存并刷新预览"); } else { - ToastUtils.show("拍照数据获取失败"); - LogUtils.e(TAG, "【拍照回调失败】拍照数据Bundle为空,无法获取图片"); - // 清理缓存目录下的裁剪临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【拍照异常清理】拍照数据获取失败,清理缓存目录裁剪临时文件:" + _mSourceCropTempFile.getPath()); - } + ToastUtils.show("获取剪裁图片失败(Bitmap解析异常)"); + LogUtils.e(TAG, "【裁剪回调失败】裁剪Bitmap解析失败或已回收"); + clearCropTempFile(); } - } - // 处理裁剪回调(REQUEST_CROP_IMAGE) - else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) { - LogUtils.d(TAG, "【裁剪回调】CROP_IMAGE_REQUEST_CODE 回调触发,开始处理裁剪结果(临时文件位于缓存目录)"); - try { - Bitmap cropBitmap = null; - // 核心修复:优先读取缓存目录下的裁剪临时文件(放弃data.getParcelableExtra,避免缩略图/空Bitmap) - LogUtils.d(TAG, "【裁剪回调】裁剪临时文件(缓存目录)校验:路径=" + _mSourceCropTempFile.getPath() + ",是否存在=" + _mSourceCropTempFile.exists() + ",文件大小=" + _mSourceCropTempFile.length() + " bytes"); - if (_mSourceCropTempFile.exists() && _mSourceCropTempFile.length() > 0) { - LogUtils.d(TAG, "【裁剪回调】缓存目录裁剪临时文件有效,开始解析Bitmap"); - // 核心修复:优化Bitmap解析选项(自动适配格式+防止OOM+避免损坏图片解析失败) - BitmapFactory.Options options = new BitmapFactory.Options(); - // 第一步:仅获取图片信息,不加载Bitmap(避免OOM,尤其是大图片) - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); - LogUtils.d(TAG, "【Bitmap解析】图片信息:格式=" + (options.outMimeType != null ? options.outMimeType : "未知") + ",原始宽高=" + options.outWidth + "x" + options.outHeight); + } else { + // 处理裁剪取消/失败 + handleOperationCancelOrFail(); + } + } - // 自动适配图片格式(PNG用ARGB_8888保留透明,JPEG用RGB_565省内存) - String imageMimeType = options.outMimeType; - options.inPreferredConfig = (imageMimeType != null && imageMimeType.contains("png")) - ? Bitmap.Config.ARGB_8888 - : Bitmap.Config.RGB_565; - LogUtils.d(TAG, "【Bitmap解析】自动适配配置:" + options.inPreferredConfig); + /** + * 处理所有操作取消/失败(resultCode != RESULT_OK) + * 职责:统一提示+清理临时文件,避免残留文件影响下次操作 + */ + private void handleOperationCancelOrFail() { + LogUtils.d(TAG, "【操作回调】操作取消或失败"); + ToastUtils.show("操作已取消"); + // 强制清理裁剪临时文件(应用私有外部目录) + clearCropTempFile(); + } - // 自动计算采样率(防止大图片OOM,最大边长限制为2048) - int maxImageSize = 2048; - int sampleRate = 1; - while (options.outWidth / sampleRate > maxImageSize || options.outHeight / sampleRate > maxImageSize) { - sampleRate *= 2; // 每次翻倍采样,确保是2的幂(BitmapFactory要求) - } - options.inSampleSize = sampleRate; - LogUtils.d(TAG, "【Bitmap解析】采样率计算完成:" + sampleRate + ",目标宽高=" + options.outWidth/sampleRate + "x" + options.outHeight/sampleRate); + /** + * 处理MIUI裁剪工具写入空文件(0字节)场景 + * 职责:精准提示用户+保留文件用于调试,适配MIUI裁剪特性 + */ + private void handleMiuiCropZeroByteFile() { + LogUtils.e(TAG, "【裁剪失败】裁剪临时文件为空(MIUI裁剪工具适配问题),建议选择「系统相机裁剪」或第三方裁剪工具"); + ToastUtils.show("裁剪失败,请选择系统相机裁剪重试"); + LogUtils.d(TAG, "【裁剪调试】保留空文件用于排查:" + _mSourceCropTempFile.getPath()); + // 不删除空文件,方便后续查看权限/路径问题 + } - // 第二步:正式加载Bitmap(关闭inJustDecodeBounds) - options.inJustDecodeBounds = false; - cropBitmap = BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); - LogUtils.d(TAG, "【Bitmap解析】裁剪Bitmap加载:" + (cropBitmap != null ? "成功" : "失败") + ",加载后大小:" + (cropBitmap != null ? cropBitmap.getByteCount()/1024 + "KB" : "0")); - } else { - ToastUtils.show("剪裁文件为空或损坏"); - LogUtils.e(TAG, "【裁剪回调失败】缓存目录裁剪临时文件无效,无法解析"); - return; - } + /** + * 辅助函数:为选图Uri添加持久化读取权限(Android4.4+) + */ + private void grantPersistableUriPermission(Uri uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getContentResolver().takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ); + LogUtils.d(TAG, "【选图权限】已为选择的图片Uri添加持久化读取权限"); + } + } - // 检查解析后的Bitmap是否有效 - if (cropBitmap != null && !cropBitmap.isRecycled()) { - LogUtils.d(TAG, "【裁剪回调】裁剪Bitmap有效,开始保存"); - saveCropBitmap(cropBitmap); - // 核心修复:保存后再次刷新预览(双重保障,确保裁剪图实时显示) - runOnUiThread(new Runnable() { + /** + * 辅助函数:解析选图Uri为File(适配不同Uri格式,兜底流复制) + */ + private File parseUriToFile(Uri uri) { + File targetFile = null; + String filePath = UriUtil.getFilePathFromUri(this, uri); + LogUtils.d(TAG, "【选图解析】Uri解析后的文件路径:" + filePath); + + // 1. 直接解析成功 + if (!TextUtils.isEmpty(filePath)) { + targetFile = new File(filePath); + } else { + // 2. 解析失败,流复制生成临时文件(应用私有外部目录,无权限问题) + targetFile = createTempFileByStreamCopy(uri); + } + return targetFile; + } + + /** + * 辅助函数:通过流复制生成选图临时文件(兜底方案,适配ContentProvider Uri) + */ + private File createTempFileByStreamCopy(Uri uri) { + File externalTempDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "SelectTemp"); + if (!externalTempDir.exists()) { + externalTempDir.mkdirs(); + LogUtils.d(TAG, "【选图解析】创建应用私有外部临时子目录:" + externalTempDir.getAbsolutePath()); + } + + File tempFile = new File(externalTempDir, "selected_temp.jpg"); + try { + if (tempFile.exists()) { + tempFile.delete(); + LogUtils.d(TAG, "【选图解析】旧临时文件已清理,准备生成新临时文件"); + } + // 流复制(适配ContentProvider Uri,无需外部存储权限) + FileUtils.copyStreamToFile(getContentResolver().openInputStream(uri), tempFile); + LogUtils.d(TAG, "【选图解析】Uri解析失败,通过流复制生成临时文件:" + tempFile.getPath()); + } catch (Exception e) { + LogUtils.e(TAG, "【选图解析】流复制生成临时文件失败:" + e.getMessage(), e); + tempFile = null; + } + return tempFile; + } + + /** + * 辅助函数:同步选图文件到预览Bean并刷新预览 + */ + private void syncSelectedFileToPreview(File selectedFile, String uriStr) { + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); + utils.saveFileToPreviewBean(selectedFile, uriStr); + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【选图同步】预览图片已更新,文件路径:" + selectedFile.getAbsolutePath()); + } + + /** + * 辅助函数:从拍照数据中获取Bitmap + */ + private Bitmap getTakePhotoBitmap(Intent data) { + Bundle extras = data.getExtras(); + LogUtils.d(TAG, "【拍照回调】拍照数据Bundle:" + (extras != null ? "非空" : "为空")); + if (extras == null) { + return null; + } + Bitmap bitmap = (Bitmap) extras.get("data"); + LogUtils.d(TAG, "【拍照回调】获取拍照Bitmap:" + (bitmap != null ? "成功" : "失败") + ",是否回收:" + (bitmap != null && bitmap.isRecycled())); + return bitmap; + } + + /** + * 辅助函数:解析裁剪临时文件为Bitmap(适配大图片OOM,MIUI裁剪结果) + */ + private Bitmap parseCropTempFileToBitmap() { + Bitmap cropBitmap = null; + if (!_mSourceCropTempFile.exists() || _mSourceCropTempFile.length() <= 100) { + return null; + } + + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + // 第一步:仅获取图片信息,不加载Bitmap(避免OOM) + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】图片信息:格式=" + (options.outMimeType != null ? options.outMimeType : "未知") + ",原始宽高=" + options.outWidth + "x" + options.outHeight); + + // 自动适配格式+计算采样率(最大边长2048,适配MIUI裁剪尺寸) + options.inPreferredConfig = getBitmapConfigByMimeType(options.outMimeType); + options.inSampleSize = calculateBitmapSampleRate(options, 2048); + + // 第二步:正式加载Bitmap + options.inJustDecodeBounds = false; + cropBitmap = BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】裁剪Bitmap加载:" + (cropBitmap != null ? "成功" : "失败") + ",加载后大小:" + (cropBitmap != null ? cropBitmap.getByteCount()/1024 + "KB" : "0")); + } catch (Exception e) { + LogUtils.e(TAG, "【Bitmap解析】裁剪临时文件解析失败:" + e.getMessage(), e); + } + return cropBitmap; + } + + /** + * 辅助函数:根据图片格式自动适配Bitmap配置(PNG保留透明,JPEG省内存) + */ + private Bitmap.Config getBitmapConfigByMimeType(String mimeType) { + return (mimeType != null && mimeType.contains("png")) + ? Bitmap.Config.ARGB_8888 + : Bitmap.Config.RGB_565; + } + + /** + * 辅助函数:计算Bitmap采样率(防止大图片OOM) + */ + private int calculateBitmapSampleRate(BitmapFactory.Options options, int maxSize) { + int sampleRate = 1; + int width = options.outWidth; + int height = options.outHeight; + while (width / sampleRate > maxSize || height / sampleRate > maxSize) { + sampleRate *= 2; // 确保是2的幂(BitmapFactory要求) + } + LogUtils.d(TAG, "【Bitmap解析】采样率计算完成:" + sampleRate + ",目标宽高=" + width/sampleRate + "x" + height/sampleRate); + return sampleRate; + } + + /** + * 辅助函数:主线程双重刷新预览(适配MIUI控件渲染耗时) + */ + private void doubleRefreshPreview() { + runOnUiThread(new Runnable() { + @Override + public void run() { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【预览刷新】第一次刷新bvPreviewBackground(立即)"); + // 延迟300ms再次刷新,确保MIUI机型加载成功 + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【裁剪回调】主线程二次刷新预览视图,确保显示最新裁剪图"); + LogUtils.d(TAG, "【预览刷新】第二次刷新bvPreviewBackground(延迟300ms)"); } - }); - } else { - ToastUtils.show("获取剪裁图片失败(Bitmap解析异常)"); - LogUtils.e(TAG, "【裁剪回调失败】裁剪Bitmap解析失败或已回收"); - // 清理缓存目录下的无效临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【裁剪异常清理】Bitmap解析失败,清理缓存目录无效临时文件"); - } + }, 300); } - } catch (OutOfMemoryError e) { - LogUtils.e(TAG, "【裁剪异常-OOM】内存溢出:" + e.getMessage()); - ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片"); - // 清理缓存目录下的临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常-未知】剪裁保存异常:" + e.getMessage(), e); - ToastUtils.show("保存时发生错误:" + e.getMessage().substring(0, 20)); - // 清理缓存目录下的临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - } finally { - // 核心修复:移除临时文件清理代码(已移至saveCropBitmap成功后清理,避免提前删除导致保存失败) - LogUtils.d(TAG, "【裁剪回调】裁剪流程结束(缓存目录临时文件清理由saveCropBitmap负责)"); - } - } - // 处理操作取消/失败(resultCode != RESULT_OK) - else if (resultCode != RESULT_OK) { - LogUtils.d(TAG, "【操作回调】操作取消或失败,requestCode: " + requestCode + ",resultCode: " + resultCode); - ToastUtils.show("操作已取消"); - // 操作取消/失败时,强制清理缓存目录下的裁剪临时文件(避免占用缓存空间+下次操作异常) - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【操作取消清理】操作取消/失败,缓存目录裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getPath()); - } + }); + } + + /** + * 辅助函数:清理裁剪临时文件(统一封装,避免重复代码) + */ + private void clearCropTempFile() { + if (_mSourceCropTempFile.exists()) { + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【临时文件清理】裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getPath()); } } + /** * 检查类型是否为图片(适配常见图片格式,避免非图片文件误处理) */ @@ -1144,46 +1782,172 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 检查并申请存储权限(修复:适配低版本API,移除Android13+依赖,多机型兼容,适配缓存目录权限) + * 检查并申请存储权限(终极修复:适配Android 14+ 应用私有外部目录权限,解决Permission denied) + * 关键适配:Android14+ 需显式申请WRITE_EXTERNAL_STORAGE,仅靠所有文件访问权限无法覆盖应用私有目录 */ private boolean checkAndRequestStoragePermission() { LogUtils.d(TAG, "【权限校验】checkAndRequestStoragePermission 触发,Android版本:" + Build.VERSION.SDK_INT); - // 核心优化:应用缓存目录(getCacheDir())无需外部存储权限,直接返回true - LogUtils.d(TAG, "【权限校验】应用缓存目录操作无需外部存储权限,跳过权限申请"); - // Android 11+(R):使用所有文件访问权限(仅针对外部存储操作,缓存目录无需) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - boolean hasPermission = Environment.isExternalStorageManager(); - LogUtils.d(TAG, "【权限校验】Android11+ 所有文件访问权限:" + (hasPermission ? "已获取" : "未获取") + "(仅影响外部存储操作)"); - if (!hasPermission) { - // 仅在需要操作外部存储时提示授权(缓存目录操作不受影响) - Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - startActivity(intent); - ToastUtils.show("请开启「所有文件访问权限」(仅用于外部图片选择/保存)"); - return false; - } - } - // Android 6.0+(M):申请读写外部存储权限(仅针对外部存储操作) - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - boolean hasReadPerm = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + // 核心修复1:Android 14+(API 34+)强制申请 WRITE_EXTERNAL_STORAGE 权限(应用私有外部目录必需) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+ 开始强化,14+ 强制要求 boolean hasWritePerm = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - LogUtils.d(TAG, "【权限校验】Android6.0+ 存储权限:读=" + hasReadPerm + ",写=" + hasWritePerm + "(仅影响外部存储操作)"); - if (!hasReadPerm || !hasWritePerm) { - // 同时申请读写权限(仅用于外部存储操作,缓存目录无需) - String[] permissions = new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - }; - ActivityCompat.requestPermissions(this, permissions, STORAGE_PERMISSION_REQUEST); - LogUtils.d(TAG, "【权限申请】已触发存储权限申请(仅用于外部图片操作),请求码:" + STORAGE_PERMISSION_REQUEST); + LogUtils.d(TAG, "【权限校验】Android14+ 应用私有外部目录必需:WRITE_EXTERNAL_STORAGE 权限=" + hasWritePerm); + if (!hasWritePerm) { + // 显式申请 WRITE_EXTERNAL_STORAGE 权限(适配应用私有外部目录) + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST); + LogUtils.d(TAG, "【权限申请】Android14+ 已触发 WRITE_EXTERNAL_STORAGE 权限申请(应用私有目录必需)"); return false; } } - // Android 6.0以下:权限默认授予(缓存目录/外部存储均无需额外操作) - LogUtils.d(TAG, "【权限校验】存储权限校验完成(缓存目录操作不受限),可正常操作"); + + // 核心修复2:Android 11+(R)保留所有文件访问权限校验(覆盖外部存储其他目录) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + boolean hasAllFilePerm = Environment.isExternalStorageManager(); + LogUtils.d(TAG, "【权限校验】Android11+ 所有文件访问权限:" + (hasAllFilePerm ? "已获取" : "未获取") + "(外部存储其他目录必需)"); + if (!hasAllFilePerm) { + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + startActivity(intent); + ToastUtils.show("请开启「所有文件访问权限」+「存储写入权限」(图片选择/裁剪必需)"); + return false; + } + } + + // Android 6.0-10:保留原读写权限校验(适配旧机型) + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + boolean hasReadPerm = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + boolean hasWritePerm = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + LogUtils.d(TAG, "【权限校验】Android6.0-10 存储权限:读=" + hasReadPerm + ",写=" + hasWritePerm); + if (!hasReadPerm || !hasWritePerm) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST); + LogUtils.d(TAG, "【权限申请】已触发存储读写权限申请"); + return false; + } + } + + // 修复3:权限校验通过后,强制刷新应用私有目录权限(解决Android14+ 权限设置后不生效问题) + refreshAppPrivateDirPermission(); + + LogUtils.d(TAG, "【权限校验】存储权限校验完成(Android14+ 应用私有目录权限已确认),可正常操作"); return true; } + /** + * 辅助函数:强制刷新应用私有目录权限(解决Android14+ 权限设置后不生效问题) + */ + private void refreshAppPrivateDirPermission() { + File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (appPrivateDir != null && appPrivateDir.exists()) { + // 递归设置目录权限(Android14+ 必需,单一目录设置无效) + setDirPermissionsRecursively(appPrivateDir); + LogUtils.d(TAG, "【权限刷新】应用私有目录权限已强制设置:" + appPrivateDir.getAbsolutePath() + ",可写=" + appPrivateDir.canWrite() + ",可读=" + appPrivateDir.canRead()); + } + } + + /** + * 递归设置目录及子目录/文件的读写权限(适配Java7 + Android全版本,解决Permission denied问题) + * 核心适配:移除Java8+特性(如Lambda、方法引用),兼容Java7语法,同时保障Android14+权限有效性 + * @param dir 要设置权限的目标目录 + */ + private void setDirPermissionsRecursively(File dir) { + // 1. 校验目录有效性(避免空指针/无效目录,Java7兼容写法) + if (dir == null || !dir.exists()) { + String dirPath = (dir != null) ? dir.getAbsolutePath() : "null"; + LogUtils.d(TAG, "【权限设置】目录无效,无需处理:" + dirPath); + return; + } + + try { + // 2. 核心:设置目录权限(Android14+ 关键,Java7兼容写法) + // 注:setReadable/setWritable 第二个参数(ownerOnly)Java7已支持,必须设为false(允许所有用户访问) + dir.setReadable(true, false); // 所有用户可读取(裁剪工具需读取文件) + dir.setWritable(true, false); // 所有用户可写入(裁剪工具需写入文件) + dir.setExecutable(false, false); // 关闭执行权限,提升安全性(目录无需执行) + + // 3. 打印目录权限状态(Java7字符串拼接写法,避免String.join) + String dirPath = dir.getAbsolutePath(); + boolean canWrite = dir.canWrite(); + boolean canRead = dir.canRead(); + String dirType = getDirTypeDesc(dir); + LogUtils.d(TAG, "【权限设置】目录权限更新完成:路径=" + dirPath + ",可写=" + canWrite + ",可读=" + canRead + ",目录类型=" + dirType); + + // 4. 递归处理子目录和文件(Java7兼容,避免Stream API) + File[] files = dir.listFiles(); + // 校验files非空(避免空指针,Java7必需) + if (files != null && files.length > 0) { + // 增强for循环(Java7支持,替代forEach) + for (File file : files) { + if (file.isDirectory()) { + // 递归处理子目录(确保所有层级权限一致) + setDirPermissionsRecursively(file); + } else { + // 处理文件:设置与目录一致的权限(Java7兼容) + file.setReadable(true, false); + file.setWritable(true, false); + file.setExecutable(false, false); + + // 5. 关键文件(裁剪临时/结果文件)单独打印日志(精准调试) + String fileName = file.getName(); + if (fileName.equals(_mSourceCropTempFileName) || fileName.equals(_mSourceCroppedFileName)) { + String filePath = file.getAbsolutePath(); + boolean fileCanWrite = file.canWrite(); + boolean fileCanRead = file.canRead(); + LogUtils.d(TAG, "【权限设置】关键文件权限更新:文件名=" + fileName + ",路径=" + filePath + ",可写=" + fileCanWrite + ",可读=" + fileCanRead); + } + } + } + } + + } catch (SecurityException e) { + // 捕获系统权限异常(如系统禁止修改权限,Java7兼容捕获) + String errMsg = "【权限设置异常】系统禁止修改目录权限:" + dir.getAbsolutePath() + ",错误信息:" + e.getMessage(); + LogUtils.e(TAG, errMsg, e); + // 提示用户(Java7兼容:用Runnable替代Lambda) + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("文件权限设置失败,请检查应用存储权限"); + } + }); + + } catch (Exception e) { + // 捕获通用异常(文件锁定、IO错误等,Java7兼容) + String errMsg = "【权限设置异常】处理目录权限时出错:" + dir.getAbsolutePath() + ",错误信息:" + e.getMessage(); + LogUtils.e(TAG, errMsg, e); + } + } + + /** + * 辅助函数:获取目录类型描述(适配Java7,无Lambda/Stream) + * @param dir 目标目录 + * @return 目录类型描述(兼容低版本语法) + */ + private String getDirTypeDesc(File dir) { + if (dir == null) { + return "未知目录"; + } + String dirPath = dir.getAbsolutePath(); + String internalDirPath = getFilesDir().getAbsolutePath(); + String externalDirPath = ""; + // 兼容Java7:先校验getExternalFilesDir非空,避免空指针 + File externalDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (externalDir != null) { + externalDirPath = externalDir.getAbsolutePath(); + } + String cacheDirPath = getCacheDir().getAbsolutePath(); + + // Java7条件判断(避免三元表达式嵌套过深,提升可读性) + if (dirPath.contains(internalDirPath)) { + return "内部存储目录(getFilesDir(),Android14+ 权限最稳定)"; + } else if (externalDir != null && dirPath.contains(externalDirPath)) { + return "应用私有外部目录(getExternalFilesDir())"; + } else if (dirPath.contains(cacheDirPath)) { + return "应用缓存目录(getCacheDir(),兜底目录)"; + } else { + return "外部存储目录(需额外权限)"; + } + } + + @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); diff --git a/powerbell/src/main/res/xml/file_provider.xml b/powerbell/src/main/res/xml/file_provider.xml index c2f91517..ff896c31 100644 --- a/powerbell/src/main/res/xml/file_provider.xml +++ b/powerbell/src/main/res/xml/file_provider.xml @@ -1,43 +1,53 @@ - - - - - - - - - - - - - + + + path="Pictures/" /> - - + + + + + + + + - - + + + + + + + + + + +