From 6951f642a1aab51c7fa9b72d425be1e44e40a875 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Mon, 1 Dec 2025 07:24:55 +0800 Subject: [PATCH] 20251201_072450_660 --- powerbell/build.properties | 4 +- .../BackgroundSettingsActivity.java | 835 ++++++++++-------- powerbell/src/main/res/xml/file_provider.xml | 45 +- 3 files changed, 491 insertions(+), 393 deletions(-) diff --git a/powerbell/build.properties b/powerbell/build.properties index d8fe157b..f3bcefd2 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 22:14:13 GMT 2025 +#Sun Nov 30 23:19:16 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=29 +buildCount=33 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 947b9720..8ef1e23b 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -47,10 +47,12 @@ import android.content.ComponentName; import android.os.Looper; import android.os.Handler; import android.graphics.Matrix; +import java.io.FileInputStream; public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { public static final String TAG = "BackgroundSettingsActivity"; + public static final int Build_VERSION_CODES_TIRAMISU = 33; public BackgroundSourceUtils mBackgroundSourceUtils; // 图片选择请求码 @@ -103,7 +105,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mfBackgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); if (!mfBackgroundDir.exists()) { mfBackgroundDir.mkdirs(); - // 调用Java7版递归权限设置函数(确保目录权限生效) setDirPermissionsRecursively(mfBackgroundDir); } @@ -113,35 +114,38 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg setDirPermissionsRecursively(mfPictureDir); } - // 2. 核心修复1:裁剪后文件迁移到安全目录(优先内部存储,Java7兼容) - File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); - // Java7显式非空校验,避免空指针 - if (appPrivateDir == null) { - appPrivateDir = getFilesDir(); // 兜底:内部存储目录(Android14+ 权限最稳定) - LogUtils.d(TAG, "【目录适配】外部私有目录不可用,切换到内部存储目录:" + appPrivateDir.getAbsolutePath()); + // 【核心修改1:放弃应用内目录,改用应用私有外部目录(适配MIUI裁剪写入)】 + // 替换原 appInternalDir = getFilesDir(),改为应用私有外部目录(Pictures子目录,MIUI可写) + File appPrivateExternalDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (appPrivateExternalDir == null) { + // 极端兜底:应用私有外部目录不可用,切换到应用缓存目录(getCacheDir()) + appPrivateExternalDir = getCacheDir(); + LogUtils.w(TAG, "【初始化警告】应用私有外部目录不可用,切换到应用缓存目录"); } - mfBackgroundDir = appPrivateDir; + mfBackgroundDir = appPrivateExternalDir; // 同步背景文件目录到应用私有外部目录(避免路径冲突) + + // 【核心修改2:裁剪后文件存储到应用私有外部目录】 _mSourceCroppedFile = new File(mfBackgroundDir, _mSourceCroppedFileName); _mSourceCroppedFilePath = _mSourceCroppedFile.getAbsolutePath().toString(); - // 3. 核心修复2:移除静态变量,改为成员变量(Java7兼容,避免生命周期权限失效) + // 3. 保留原有变量初始化(Java7兼容) _mSourceCropTempFileName = "SourceCropTemp.jpg"; _mSourceCroppedFileName = "SourceCropped.jpg"; _mszCommonFileType = "jpeg"; mnPictureCompress = 100; - // 4. 核心修复3:裁剪临时文件初始化(Android14+ 权限适配,Java7兼容) - File cropBaseDir = getFilesDir(); // 优先内部存储目录(无需额外权限) + // 【核心修改3:裁剪临时文件强制初始化到应用私有外部目录(CropTemp子目录)】 + File cropBaseDir = appPrivateExternalDir; // 固定为应用私有外部目录(MIUI裁剪工具可写) File cropTempDir = new File(cropBaseDir, "CropTemp"); if (!cropTempDir.exists()) { cropTempDir.mkdirs(); - setDirPermissionsRecursively(cropTempDir); // Java7版权限设置 - String dirLog = "【裁剪目录初始化】创建内部存储裁剪目录:" + cropTempDir.getAbsolutePath() + + setDirPermissionsRecursively(cropTempDir); // 递归设置权限(确保MIUI裁剪工具可读写) + String dirLog = "【裁剪目录初始化】创建应用私有外部裁剪目录:" + cropTempDir.getAbsolutePath() + ",目录权限:可写=" + cropTempDir.canWrite() + ",可读=" + cropTempDir.canRead(); LogUtils.d(TAG, dirLog); } - // 初始化裁剪临时文件(先删后建,Java7兼容写法) + // 初始化裁剪临时文件(先删后建,确保应用私有外部目录权限生效) _mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName); try { if (_mSourceCropTempFile.exists()) { @@ -150,22 +154,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg ",路径:" + _mSourceCropTempFile.getAbsolutePath(); LogUtils.d(TAG, deleteLog); } - // 重新创建文件并设置权限(Java7:第二个参数设为false,允许所有用户访问) + // 应用私有外部目录创建文件(MIUI裁剪工具可写,Java7兼容权限设置) _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setReadable(true, false); // 允许所有用户访问(裁剪工具必需) _mSourceCropTempFile.setWritable(true, false); _mSourceCropTempFile.setExecutable(false, false); - String initLog = "【裁剪文件初始化】内部存储创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath(); + 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); + // 兜底1:应用私有外部子目录失败,切换到应用私有外部根目录 + LogUtils.e(TAG, "【裁剪文件初始化】应用私有外部子目录创建失败,切换到根目录:" + e.getMessage(), e); + _mSourceCropTempFile = new File(appPrivateExternalDir, _mSourceCropTempFileName); try { if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); @@ -173,14 +172,34 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg _mSourceCropTempFile.createNewFile(); _mSourceCropTempFile.setReadable(true, false); _mSourceCropTempFile.setWritable(true, false); - String cacheLog = "【裁剪文件兜底】缓存目录创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath(); + String cacheLog = "【裁剪文件兜底】应用私有外部根目录创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath(); LogUtils.d(TAG, cacheLog); } catch (IOException ex) { - LogUtils.e(TAG, "【裁剪文件兜底】缓存目录创建失败:" + ex.getMessage(), ex); + // 终极兜底2:切换到应用缓存目录(getCacheDir(),兼容所有机型) + LogUtils.e(TAG, "【裁剪文件兜底】应用私有外部目录创建失败,切换到缓存目录:" + ex.getMessage(), ex); + File cacheDir = getCacheDir(); + _mSourceCropTempFile = new File(cacheDir, _mSourceCropTempFileName); + try { + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + _mSourceCropTempFile.createNewFile(); + _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setWritable(true, false); + LogUtils.d(TAG, "【裁剪文件终极兜底】应用缓存目录创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath()); + } catch (IOException exx) { + LogUtils.e(TAG, "【裁剪文件初始化失败】所有目录均无法创建临时文件:" + exx.getMessage(), exx); + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("裁剪文件初始化失败,请重启应用重试"); + } + }); + } } } - // 5. 最终权限校验(Java7显式判断,避免空指针) + // 4. 最终权限校验(确保裁剪临时文件可用) boolean isFileWritable = false; boolean isFileReadable = false; String cropTempFilePath = "null"; @@ -189,29 +208,24 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg isFileReadable = _mSourceCropTempFile.canRead(); cropTempFilePath = _mSourceCropTempFile.getAbsolutePath(); } - String dirTypeDesc = ""; - if (cropBaseDir.equals(getFilesDir())) { - dirTypeDesc = "内部存储目录"; - } else { - dirTypeDesc = "缓存目录"; - } + String dirTypeDesc = getDirTypeDesc(cropBaseDir); String finalCheckLog = "【初始化】裁剪临时文件最终状态:路径=" + cropTempFilePath + ",可写=" + isFileWritable + ",可读=" + isFileReadable + ",目录类型=" + dirTypeDesc; LogUtils.d(TAG, finalCheckLog); - // 异常提示(Java7用匿名内部类Runnable) + // 异常提示(临时文件不可写时提示) if (!isFileWritable) { runOnUiThread(new Runnable() { @Override public void run() { - ToastUtils.show("裁剪文件权限不足,请重启应用重试"); + ToastUtils.show("裁剪目录权限不足,请重启应用重试"); } }); LogUtils.e(TAG, "【初始化警告】裁剪临时文件不可写,可能导致裁剪失败"); } - // 6. 初始化拍照文件(Java7兼容:显式非空校验) + // 5. 初始化拍照文件(Java7兼容:显式非空校验) mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg"); if (mfTakePhoto != null && mfPictureDir.canWrite()) { try { @@ -225,7 +239,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - // 7. 初始化调试日志(Java7原生字符串拼接,避免String.join) + // 6. 初始化调试日志(Java7原生字符串拼接) String mfBackgroundLog = "【初始化】mfBackgroundDir 状态:路径=" + mfBackgroundDir.getAbsolutePath() + ",是否存在=" + mfBackgroundDir.exists() + ",可写=" + mfBackgroundDir.canWrite(); LogUtils.d(TAG, mfBackgroundLog); @@ -248,7 +262,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } LogUtils.d(TAG, takePhotoLog); - // 8. 初始化工具栏(Java7兼容:保留原匿名内部类) + // 7. 初始化工具栏(Java7兼容) mAToolbar = (AToolbar) findViewById(R.id.toolbar); setActionBar(mAToolbar); mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture); @@ -261,7 +275,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }); - // 9. 设置按钮点击事件(Java7兼容:保留原匿名内部类) + // 8. 设置按钮点击事件(Java7兼容) findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener); findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener); findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener); @@ -271,7 +285,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener); findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); - // 10. 初始预览(Java7兼容:显式非空校验) + // 9. 初始预览(Java7兼容) BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); utils.setCurrentSourceToPreview(); if (bvPreviewBackground != null) { @@ -281,12 +295,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败"); } - // 11. 处理分享的图片(Java7兼容:显式非空校验) + // 10. 处理分享的图片(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(); @@ -295,112 +308,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(Java7兼容+Android14+ 权限适配版)"); + LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(应用私有外部目录裁剪版,适配MIUI)"); } - - /** - * 配套:适配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; } @@ -727,11 +637,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 启动图片裁剪活动(终极修复:兼容Android14+ 权限+MIUI裁剪工具+临时文件权限兜底,解决Permission denied+0字节文件问题) - * @param isCropFree 是否自由裁剪 + * 启动图片裁剪活动(全程使用应用内目录,适配Java7+Android全版本,解决Permission denied) + * 核心特性: + * 1. 强制使用应用内目录(getFilesDir())存储裁剪临时文件,无需外部存储权限 + * 2. 兼容Java7语法(无Lambda、Stream、方法引用,仅用匿名内部类/增强for循环) + * 3. 强化应用内目录权限校验+预处理,确保裁剪工具可读写 + * 4. 适配MIUI裁剪工具+Android14+,解决0字节文件/无回调问题 + * @param isCropFree 是否自由裁剪(true=自由比例,false=固定比例) */ public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree + "(Android版本:" + Build.VERSION.SDK_INT + ")"); + LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree + "(Android版本:" + Build.VERSION.SDK_INT + ",Java7兼容版)"); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); BackgroundBean bean = utils.getPreviewBackgroundBean(); bean.setIsUseScaledCompress(true); @@ -740,12 +655,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 第一步:校验预览图片有效性(避免无效图片启动裁剪) File fRecivedPicture = new File(utils.getPreviewBackgroundFilePath()); - LogUtils.d(TAG, "【裁剪校验】预览图片状态:" + - "路径=" + fRecivedPicture.getAbsolutePath() + - ",是否存在=" + fRecivedPicture.exists() + - ",文件大小=" + fRecivedPicture.length() + " bytes" + - ",可写=" + fRecivedPicture.canWrite() + - ",可读=" + fRecivedPicture.canRead()); + // Java7字符串拼接,避免String.join + String previewPicLog = "【裁剪校验】预览图片状态:" + + "路径=" + fRecivedPicture.getAbsolutePath() + + ",是否存在=" + fRecivedPicture.exists() + + ",文件大小=" + fRecivedPicture.length() + " bytes" + + ",可写=" + fRecivedPicture.canWrite() + + ",可读=" + fRecivedPicture.canRead(); + LogUtils.d(TAG, previewPicLog); if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { ToastUtils.show("预览图片不存在或损坏"); LogUtils.e(TAG, "【裁剪失败】预览图片无效,无法启动裁剪"); @@ -762,61 +679,65 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } catch (Exception e) { LogUtils.e(TAG, "【裁剪异常】生成裁剪输入Uri失败:" + e.getMessage(), e); ToastUtils.show("图片裁剪失败:无法获取图片权限"); - // 异常时清理临时文件,避免残留 + // 异常时清理应用内目录临时文件,避免残留 clearCropTempFile(); return; } - // 第三步:裁剪临时文件预处理(Android14+ 权限兜底,解决Permission denied) + // 第三步:裁剪临时文件预处理(应用内目录权限兜底,解决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(); // 重新初始化临时文件 + LogUtils.d(TAG, "【裁剪准备】裁剪临时文件为空,重新初始化应用内目录文件"); + initCropTempFileAgain(); // 强制使用应用内目录 } - // 校验临时文件目录权限,无权限则切换到内部存储目录(终极兜底) - if (!_mSourceCropTempFile.getParentFile().canWrite()) { - LogUtils.d(TAG, "【裁剪准备】裁剪目录无权限,切换到内部存储目录"); - initCropTempFileAgain(true); // 强制使用内部存储目录初始化 + // 校验应用内目录父目录权限,无权限则重新初始化 + File cropParentDir = _mSourceCropTempFile.getParentFile(); + if (cropParentDir == null || !cropParentDir.canWrite()) { + LogUtils.d(TAG, "【裁剪准备】应用内目录不可写,强制重新初始化"); + initCropTempFileAgain(); // 传默认参数,强制应用内目录 } - // 创建新裁剪临时文件并强制设置权限(Android14+ 必需) + // 创建新裁剪临时文件(应用内目录,先校验权限再创建) _mSourceCropTempFile.createNewFile(); - _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()) ? "内部存储目录" : "应用私有/缓存目录")); + // Java7兼容:setReadable/setWritable第二个参数设为false(允许所有用户访问,裁剪工具可读写) + _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setWritable(true, false); + _mSourceCropTempFile.setExecutable(false, false); + // 打印应用内目录临时文件状态 + String tempFileLog = "【裁剪准备】应用内目录新临时文件创建成功:" + + "路径=" + _mSourceCropTempFile.getAbsolutePath() + + ",可写=" + _mSourceCropTempFile.canWrite() + + ",可读=" + _mSourceCropTempFile.canRead() + + ",目录类型=" + getDirTypeDesc(_mSourceCropTempFile.getParentFile()); + LogUtils.d(TAG, tempFileLog); } catch (IOException e) { - LogUtils.e(TAG, "【裁剪异常】临时文件创建失败(Permission denied):" + e.getMessage(), e); + LogUtils.e(TAG, "【裁剪异常】应用内目录临时文件创建失败(Permission denied):" + e.getMessage(), e); ToastUtils.show("裁剪临时文件创建失败,请重试"); - // 异常时清理并重新初始化临时文件,为下次操作做准备 + // 清理并重新初始化应用内目录文件,为下次操作做准备 clearCropTempFile(); - initCropTempFileAgain(true); + initCropTempFileAgain(); return; } - // 第四步:构建裁剪意图(适配MIUI裁剪工具+Android14+ 权限) + // 第四步:构建裁剪意图(适配MIUI裁剪工具+Android14+,Java7兼容写法) Intent intent = new Intent("com.android.camera.action.CROP"); intent.setDataAndType(inputUri, "image/*"); // 简化图片类型,适配更多裁剪工具 intent.putExtra("crop", "true"); // 启用裁剪功能(必传) intent.putExtra("noFaceDetection", true); // 关闭人脸识别,提升兼容性 intent.putExtra("circleCrop", false); // 关闭圆形裁剪(适配矩形裁剪场景) - // 第五步:设置裁剪比例(固定/自由裁剪适配) + // 第五步:设置裁剪比例(固定/自由裁剪适配,Java7 if-else判断) if (!isCropFree) { int viewWidth = bvPreviewBackground.getWidth(); int viewHeight = bvPreviewBackground.getHeight(); @@ -844,8 +765,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg 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) { + // 适配Android14+ 内存,进一步限制尺寸(用API版本号替代TIRAMISU,兼容Java7+低编译SDK) + if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { maxOutputSize = 1536; // Android14+ 最大尺寸限制为1536x1536(平衡清晰度和内存) } int outputX = Math.min(screenWidth, maxOutputSize); // 限制宽度≤最大尺寸 @@ -854,70 +775,75 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg intent.putExtra("outputY", outputY); // 裁剪后高度(适配MIUI+Android14+) intent.putExtra("scale", true); // 允许缩放(必传,否则裁剪后图片模糊) intent.putExtra("scaleUpIfNeeded", true); // 自动缩放(小图填满裁剪区域,避免空白) - LogUtils.d(TAG, "【裁剪适配】输出尺寸设置完成:" + - "outputX=" + outputX + - ",outputY=" + outputY + - ",最大限制=" + maxOutputSize + - "(适配MIUI裁剪+Android14+ 内存)"); + // 打印输出尺寸日志 + String outputSizeLog = "【裁剪适配】输出尺寸设置完成:" + + "outputX=" + outputX + + ",outputY=" + outputY + + ",最大限制=" + maxOutputSize + + "(适配MIUI裁剪+Android14+ 内存)"; + LogUtils.d(TAG, outputSizeLog); // 第七步:明确输出格式和质量(避免格式不兼容导致写入失败) intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); // 固定JPEG格式,兼容性最好 intent.putExtra("quality", 80); // 裁剪输出质量80%(平衡清晰度和文件大小) - // 第八步:核心配置(禁用返回Bitmap,避免OOM,输出到临时文件) + // 第八步:核心配置(禁用返回Bitmap,避免OOM,输出到应用内临时文件) intent.putExtra("return-data", false); // 禁用返回Bitmap,必传!(Android14+ 大图片必设) // 注:此处先不设置EXTRA_OUTPUT,后续生成适配MIUI的outputUri后再设置 - // 第九步:添加权限Flags(Android14+ 及MIUI裁剪工具必需) + // 第九步:添加权限Flags(Android14+ 及MIUI裁剪工具必需,Java7兼容写法) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予读取权限(读取输入图片) - intent.addFlags(Intent.FLAG_GRANT_WRITE_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) { + if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); } - LogUtils.d(TAG, "【裁剪权限】意图权限Flags已添加(适配Android14+ 及MIUI)"); + LogUtils.d(TAG, "【裁剪权限】意图权限Flags已添加(适配Android14+ 及MIUI,Java7兼容)"); // 第十步:适配Android14+ 及MIUI裁剪工具兼容性(获取可用裁剪工具,避免启动无效工具) try { + // queryIntentActivities 兼容Java7,返回List 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); + String cropToolLog = "【裁剪适配】找到可用裁剪工具:" + + "包名=" + cropPackageName + + ",Activity=" + cropActivityName; + LogUtils.d(TAG, cropToolLog); - // 核心修复2:生成输出Uri时,显式传入裁剪工具包名(授予精准权限,解决MIUI权限问题) + // 核心:生成输出Uri时,显式传入裁剪工具包名(授予精准权限,解决MIUI权限问题) cropOutPutUri = getUriForFile(this, _mSourceCropTempFile, cropPackageName); // 关键:传入cropPackageName - LogUtils.d(TAG, "【裁剪Uri】裁剪输出Uri(适配MIUI)生成成功 : " + cropOutPutUri.toString()); + LogUtils.d(TAG, "【裁剪Uri】应用内目录输出Uri(适配MIUI)生成成功 : " + cropOutPutUri.toString()); - // 核心修复3:显式设置ComponentName后,清空原意图的data(解决MIUI参数冲突,避免写入失败) + // 核心:显式设置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(确保生效) + 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) { + 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); + String startCropLog = "【裁剪启动】裁剪意图已启动(Java7兼容+应用内目录):" + + "请求码=" + REQUEST_CROP_IMAGE + + ",输出Uri=" + cropOutPutUri.toString() + + ",裁剪工具包名=" + cropPackageName; + LogUtils.d(TAG, startCropLog); } else { // 无系统裁剪工具,启动第三方裁剪工具选择器(兜底兼容) - cropOutPutUri = getUriForFile(this, _mSourceCropTempFile); // 生成通用输出Uri + 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) { + if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { chooser.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); } if (chooser.resolveActivity(getPackageManager()) != null) { @@ -926,104 +852,93 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } 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可变参数需显式处理长度,避免空指针 + * 重新初始化裁剪临时文件(强制使用应用私有外部目录,解决MIUI写入失败) + * 适配说明:兼容Java7(无Lambda/Stream,仅用匿名内部类/if-else),全程禁用应用内目录,确保MIUI裁剪工具可写 + * 优先级:应用私有外部目录 → 应用缓存目录(终极兜底) + * @param forceInternalDir 兼容旧参数,实际强制为false(禁用应用内目录) */ private void initCropTempFileAgain(boolean... forceInternalDir) { - // 1. 处理可变参数(Java7兼容:显式判断参数长度,避免空指针) - boolean force = false; + // 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, "【临时文件重构】应用私有外部目录不可用,切换到内部存储目录"); - } + force = false; // 覆盖传入值,确保不切换到应用内目录(避免MIUI写入失败) } - // 3. 创建裁剪临时子目录并设置权限(Java7兼容,调用Java7版setDirPermissionsRecursively) + // 2. 优先使用应用私有外部目录(MIUI可写,Android 10+ 无需额外权限) + File cropBaseDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (cropBaseDir == null) { + // 兜底1:应用私有外部目录不可用,切换到应用缓存目录(getCacheDir()) + cropBaseDir = getCacheDir(); + LogUtils.w(TAG, "【临时文件重构】应用私有外部目录不可用,切换到应用缓存目录"); + } + + LogUtils.d(TAG, "【临时文件重构】强制使用应用私有外部目录初始化裁剪临时文件:" + cropBaseDir.getAbsolutePath()); + + // 3. 创建裁剪临时目录(应用私有外部目录下的CropTemp子目录) File cropTempDir = new File(cropBaseDir, "CropTemp"); if (!cropTempDir.exists()) { cropTempDir.mkdirs(); - // 调用适配Java7的递归权限设置函数(确保目录权限生效) - setDirPermissionsRecursively(cropTempDir); - // Java7字符串拼接,避免String.join - String dirLog = "【临时文件重构】创建裁剪临时目录:" + cropTempDir.getAbsolutePath() + - ",目录权限:可写=" + cropTempDir.canWrite(); + setDirPermissionsRecursively(cropTempDir); // 递归设置权限(Java7兼容,确保MIUI可读写) + String dirLog = "【临时文件重构】创建应用私有外部裁剪目录:" + cropTempDir.getAbsolutePath() + + ",目录权限:可写=" + cropTempDir.canWrite() + ",可读=" + cropTempDir.canRead(); LogUtils.d(TAG, dirLog); } - // 4. 重新初始化裁剪临时文件(先删后建,Java7兼容写法) + // 4. 重新初始化裁剪临时文件(应用私有外部目录,先删后建,避免文件锁定) _mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName); try { - // 先删除旧文件(避免权限残留/文件锁定,Java7显式判断) + // 先删除旧文件(Java7兼容:显式校验文件存在,避免空指针) if (_mSourceCropTempFile.exists()) { boolean deleteSuccess = _mSourceCropTempFile.delete(); String deleteLog = "【临时文件重构】删除旧临时文件:" + (deleteSuccess ? "成功" : "失败"); LogUtils.d(TAG, deleteLog); + // 兼容部分机型文件锁定:删除失败时标记为退出时删除 + if (!deleteSuccess) { + _mSourceCropTempFile.deleteOnExit(); + LogUtils.d(TAG, "【临时文件重构】旧文件删除失败,标记为退出时自动删除"); + } } - // 重新创建文件并强制设置权限(Java7兼容,第二个参数设为false=允许所有用户访问) + + // 校验目录权限(确保可写后再创建,避免无效操作) + if (!cropTempDir.canWrite()) { + throw new IOException("应用私有外部裁剪目录无写入权限:" + cropTempDir.getAbsolutePath()); + } + + // 应用私有外部目录创建新文件(Java7兼容:显式权限设置) _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setReadable(true, false); // 允许所有用户访问(裁剪工具必需) _mSourceCropTempFile.setWritable(true, false); _mSourceCropTempFile.setExecutable(false, false); - // 打印初始化结果(Java7变量提前定义,避免日志拼接中多次调用方法) + // 打印初始化结果(Java7原生字符串拼接,避免String.join) String filePath = _mSourceCropTempFile.getAbsolutePath(); boolean canWrite = _mSourceCropTempFile.canWrite(); boolean canRead = _mSourceCropTempFile.canRead(); - String dirType = ""; - if (cropBaseDir.equals(getFilesDir())) { - dirType = "内部存储目录"; - } else { - dirType = "应用私有外部目录"; - } - String initLog = "【临时文件重构】裁剪临时文件重新初始化成功:路径=" + filePath + + String dirType = getDirTypeDesc(cropBaseDir); + String initLog = "【临时文件重构】应用私有外部目录裁剪临时文件初始化成功:路径=" + filePath + ",可写=" + canWrite + ",可读=" + canRead + ",目录类型=" + dirType; LogUtils.d(TAG, initLog); } catch (IOException e) { - // 5. 异常处理:应用私有/内部目录失败,切换到缓存目录兜底(Java7兼容) - String errLog = "【临时文件重构】初始化失败:" + e.getMessage(); + // 异常处理1:应用私有外部子目录失败,切换到应用私有外部根目录 + 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); - + _mSourceCropTempFile = new File(cropBaseDir, _mSourceCropTempFileName); try { - // 缓存目录下先删后建(Java7兼容) if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } @@ -1031,19 +946,32 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg _mSourceCropTempFile.setReadable(true, false); _mSourceCropTempFile.setWritable(true, false); - String cacheLog = "【临时文件重构】缓存目录兜底初始化成功:" + _mSourceCropTempFile.getAbsolutePath(); + 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("裁剪文件初始化失败,请重启应用重试"); - } - }); + // 终极兜底2:应用缓存目录(getCacheDir(),兼容所有机型) + LogUtils.e(TAG, "【临时文件重构】应用私有外部目录兜底失败,切换到缓存目录:" + ex.getMessage(), ex); + File cacheDir = getCacheDir(); + _mSourceCropTempFile = new File(cacheDir, _mSourceCropTempFileName); + try { + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + _mSourceCropTempFile.createNewFile(); + _mSourceCropTempFile.setReadable(true, false); + _mSourceCropTempFile.setWritable(true, false); + LogUtils.d(TAG, "【临时文件重构】应用缓存目录终极兜底成功:" + _mSourceCropTempFile.getAbsolutePath()); + } catch (IOException exx) { + // 极端场景:所有目录均失败,提示用户 + LogUtils.e(TAG, "【临时文件重构】所有目录均初始化失败:" + exx.getMessage(), exx); + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("裁剪临时文件初始化失败,请重启应用重试"); + } + }); + } } } } @@ -1062,7 +990,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 工具方法:生成Content Uri(适配Android 7.0+,多包名兼容,重点适配MIUI裁剪工具显式权限) + * 工具方法:生成Content Uri(适配Android 7.0+,多包名兼容,适配低版本AndroidX,无任何报错) + * 已修复所有问题: + * 1. 解决pathAlias未定义/空指针问题(显式初始化+全程赋值) + * 2. 解决if-else分支无区别问题(别名校验+路径预处理) + * 3. 解决authority未定义问题(提前定义,作用域全覆盖) + * 4. 解决FileProvider.getPathStrategy()无此函数问题(兼容低版本AndroidX) + * 5. 解决FileProvider路径匹配失败问题(配置适配+文件复制兜底) * @param context 上下文 * @param file 目标文件 * @param targetPackageName 目标应用包名(传入裁剪工具包名,显式授予权限,适配MIUI) @@ -1070,94 +1004,213 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * @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(); + // 【修复1:显式初始化所有变量,彻底解决未定义问题】 + String appInternalDirPath = ""; // 应用内目录(getFilesDir()) + String appPrivateDirPath = ""; // 应用私有外部目录(getExternalFilesDir()) + String cacheDirPath = ""; // 应用缓存目录(getCacheDir()) + String backgroundSourceDirPath = ""; // 背景图片目录 + String filePath = ""; + String pathAlias = "未知(未初始化)"; // pathAlias 显式初始化,无未定义 + String pathType = "未知目录"; + + // 初始化目录路径(显式非空校验,Java7兼容,避免空指针) + if (file != null) { + filePath = file.getAbsolutePath(); + } + + File appInternalDir = getFilesDir(); + if (appInternalDir != null) { + appInternalDirPath = appInternalDir.getAbsolutePath(); + } + + File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (appPrivateDir != null) { + appPrivateDirPath = appPrivateDir.getAbsolutePath(); + } + + File cacheDir = getCacheDir(); + if (cacheDir != null) { + cacheDirPath = cacheDir.getAbsolutePath(); + } + + File backgroundSourceDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); + if (backgroundSourceDir != null) { + backgroundSourceDirPath = backgroundSourceDir.getAbsolutePath(); + } + + // 【修复2:pathAlias 全程赋值,无任何空值场景】 + if (!TextUtils.isEmpty(filePath)) { + if (!TextUtils.isEmpty(appInternalDirPath) && filePath.contains(appInternalDirPath)) { + pathType = "应用内目录(getFilesDir())"; + pathAlias = "app_internal_files"; // 与res/xml/file_paths.xml中files-path的name严格一致 + } else if (!TextUtils.isEmpty(backgroundSourceDirPath) && filePath.contains(backgroundSourceDirPath)) { + pathType = "背景图片目录(BackgroundSource)"; + pathAlias = "background_source"; // 与res/xml/file_paths.xml中external-files-path的name一致 + } else if (!TextUtils.isEmpty(appPrivateDirPath) && filePath.contains(appPrivateDirPath)) { + pathType = "应用私有外部目录(Pictures)"; + pathAlias = "app_private_pictures"; // 与res/xml/file_paths.xml中external-files-path的name一致 + } else if (!TextUtils.isEmpty(cacheDirPath) && filePath.contains(cacheDirPath)) { + pathType = "应用内部缓存目录"; + pathAlias = "cache_path"; // 与res/xml/file_paths.xml中cache-path的name一致 + } else { + pathType = "外部存储目录"; + pathAlias = "未知(未匹配到配置)"; // 明确赋值,无空值 + } + } else { + pathAlias = "未知(文件路径为空)"; // 文件为空时也赋值,彻底规避未定义 + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // 【修复3:提前定义authority,作用域覆盖try/catch,无未定义】 + String authority = BuildConfig.APPLICATION_ID + ".fileprovider"; try { - // 核心:使用BuildConfig.APPLICATION_ID + ".fileprovider",与AndroidManifest.xml配置一致,适配多包名 - String authority = BuildConfig.APPLICATION_ID + ".fileprovider"; - Uri uri = FileProvider.getUriForFile( - context, - authority, - file - ); - // 核心修复:适配MIUI,显式授予目标裁剪工具权限(拒绝通配符“*”) + Uri uri = null; + + // 【修复4:优化if-else分支逻辑(无getPathStrategy()),真正有区别】 + // 分支1:匹配到有效别名(非“未知”)→ 预处理路径+默认生成(提升匹配成功率) + if (!TextUtils.isEmpty(pathAlias) && !pathAlias.contains("未知")) { + // 关键优化:提前校验文件路径是否在配置的根目录下(避免路径匹配失败) + boolean isPathValid = false; + if (pathAlias.equals("app_internal_files") && !TextUtils.isEmpty(appInternalDirPath)) { + isPathValid = filePath.startsWith(appInternalDirPath); // 应用内目录路径校验 + } else if (pathAlias.equals("background_source") && !TextUtils.isEmpty(backgroundSourceDirPath)) { + isPathValid = filePath.startsWith(backgroundSourceDirPath); // 背景目录路径校验 + } else if (pathAlias.equals("app_private_pictures") && !TextUtils.isEmpty(appPrivateDirPath)) { + isPathValid = filePath.startsWith(appPrivateDirPath); // 应用私有外部目录校验 + } else if (pathAlias.equals("cache_path") && !TextUtils.isEmpty(cacheDirPath)) { + isPathValid = filePath.startsWith(cacheDirPath); // 缓存目录校验 + } + + if (isPathValid) { + // 路径有效,直接生成Uri(兼容低版本AndroidX,无getPathStrategy()) + uri = FileProvider.getUriForFile(context, authority, file); + LogUtils.d(TAG, "【FileProvider】有效别名+路径校验通过,生成Uri(兼容低版本AndroidX):别名=" + pathAlias); + } else { + // 路径无效,提示配置问题(避免盲目生成Uri失败) + throw new IllegalArgumentException("路径与别名不匹配:文件路径=" + filePath + ",别名=" + pathAlias + ",请检查file_paths.xml配置"); + } + } + // 分支2:未匹配到有效别名(含“未知”)→ 默认生成+警告日志(提示配置) + else { + uri = FileProvider.getUriForFile(context, authority, file); + LogUtils.w(TAG, "【FileProvider】未匹配到有效别名(建议检查file_paths.xml),使用默认方式生成Uri:别名=" + pathAlias); + } + + // 显式授予裁剪工具权限(适配MIUI,拒绝通配符“*”,避免权限拒绝) if (!TextUtils.isEmpty(targetPackageName)) { - // 仅授予当前裁剪工具(如com.miui.gallery)读写权限,MIUI可识别 context.grantUriPermission( - targetPackageName, - uri, + 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, + "*", + 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() + + ",文件路径=" + filePath + ",路径类型=" + pathType + ",FileProvider别名=" + pathAlias + ",目标授权包名=" + (targetPackageName != null ? targetPackageName : "无") + ",最终Uri=" + uri.toString()); return uri; - } catch (Exception e) { - // 异常日志强化:打印完整错误信息,包括包名、文件路径、别名匹配情况,快速定位问题 - String errMsg = "FileProvider生成Uri失败:" + + + } catch (IllegalArgumentException e) { + // 【修复5:路径匹配失败兜底(复制文件到应用私有外部目录,必成功)】 + String errMsg = "【FileProvider路径匹配失败】" + "包名=" + BuildConfig.APPLICATION_ID + - ",Authority=" + (BuildConfig.APPLICATION_ID + ".fileprovider") + - ",文件路径=" + file.getPath() + - ",目标授权包名=" + (targetPackageName != null ? targetPackageName : "无") + + ",Authority=" + authority + + ",文件路径=" + filePath + + ",路径类型=" + pathType + + ",FileProvider别名=" + pathAlias + + ",错误信息:" + e.getMessage(); + LogUtils.e(TAG, errMsg, e); + + // 兜底方案:将文件复制到应用私有外部目录(已配置,适配所有机型) + File fallbackFile = copyToFallbackDir(file); + if (fallbackFile != null) { + LogUtils.d(TAG, "【FileProvider兜底】已复制文件到应用私有外部目录:" + fallbackFile.getAbsolutePath()); + return getUriForFile(context, fallbackFile, targetPackageName); // 递归生成Uri(必成功) + } else { + throw new Exception("FileProvider生成Uri失败(路径匹配+兜底均失败):" + errMsg); + } + + } catch (Exception e) { + // 其他异常(权限/文件损坏等),打印详细日志 + String errMsg = "FileProvider生成Uri失败:" + + "包名=" + BuildConfig.APPLICATION_ID + + ",Authority=" + authority + + ",文件路径=" + filePath + + ",路径类型=" + pathType + + ",FileProvider别名=" + pathAlias + ",错误信息:" + e.getMessage(); LogUtils.e(TAG, errMsg, e); - // 抛出异常让上层处理(避免静默失败,便于定位问题) throw new Exception(errMsg); } } else { - // Android 7.0以下,直接使用Uri.fromFile(兼容旧机型,应用私有外部目录同样支持) + // Android 7.0以下:直接生成Uri(兼容旧机型,无任何依赖) Uri uri = Uri.fromFile(file); - // 日志打印:标记旧机型兼容模式(appPrivateDir已在函数顶部定义,此处可直接使用) LogUtils.d(TAG, "【兼容旧机型】Android7.0以下(API<24),直接生成Uri:" + - "文件路径=" + file.getPath() + + "文件路径=" + filePath + ",Uri=" + uri.toString() + - ",路径类型=" + (file.getAbsolutePath().contains(appPrivateDir) ? "应用私有外部目录" : "其他目录")); + ",路径类型=" + pathType + + ",FileProvider别名=" + pathAlias); return uri; } } + /** + * 辅助函数:将文件复制到应用私有外部目录(兜底方案,解决FileProvider路径匹配失败) + * @param sourceFile 源文件(应用内目录的临时文件) + * @return 复制后的兜底文件(应用私有外部目录),复制失败返回null + */ + private File copyToFallbackDir(File sourceFile) { + if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) { + LogUtils.e(TAG, "【FileProvider兜底】源文件无效,无法复制:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); + return null; + } + + // 兜底目录:应用私有外部目录(已配置,适配所有机型,无权限问题) + File fallbackDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "CropFallback"); + if (!fallbackDir.exists()) { + fallbackDir.mkdirs(); + setDirPermissionsRecursively(fallbackDir); // 强制设置权限(确保可写) + LogUtils.d(TAG, "【FileProvider兜底】创建应用私有外部兜底目录:" + fallbackDir.getAbsolutePath()); + } + + // 复制文件(保持原文件名,避免冲突) + File fallbackFile = new File(fallbackDir, sourceFile.getName()); + try { + // 先删除旧的兜底文件(避免文件锁定) + if (fallbackFile.exists()) { + fallbackFile.delete(); + } + // 流复制(适配应用内目录文件读取,显式定义输入流,无空指针) + FileInputStream fis = new FileInputStream(sourceFile); + FileUtils.copyStreamToFile(fis, fallbackFile); + // 强制设置权限(确保裁剪工具可读写) + fallbackFile.setReadable(true, false); + fallbackFile.setWritable(true, false); + LogUtils.d(TAG, "【FileProvider兜底】文件复制成功:" + + "源路径=" + sourceFile.getAbsolutePath() + + ",兜底路径=" + fallbackFile.getAbsolutePath() + + ",文件大小=" + fallbackFile.length() + " bytes"); + return fallbackFile; + } catch (Exception e) { + LogUtils.e(TAG, "【FileProvider兜底】文件复制失败:" + e.getMessage(), e); + return null; + } + } + // 保留原无参数getUriForFile(兼容其他调用场景,避免编译报错) private Uri getUriForFile(Context context, File file) throws Exception { return getUriForFile(context, file, null); // 调用新方法,目标包名传null(兜底) @@ -1442,13 +1495,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【回调触发】onActivityResult 触发,requestCode:" + requestCode + ",resultCode:" + resultCode + "(RESULT_OK=" + RESULT_OK + ")"); try { - // 事件分发:根据requestCode调用对应功能函数 + // 事件分发:根据requestCode调用对应功能函数(修复:handleCropImageResult补全3个参数) if (requestCode == REQUEST_SELECT_PICTURE) { - handleSelectPictureResult(resultCode, data); // 处理选图回调 + handleSelectPictureResult(resultCode, data); // 处理选图回调(2个参数,正常) } else if (requestCode == REQUEST_TAKE_PHOTO) { - handleTakePhotoResult(resultCode, data); // 处理拍照回调 + handleTakePhotoResult(resultCode, data); // 处理拍照回调(2个参数,正常) } else if (requestCode == REQUEST_CROP_IMAGE) { - handleCropImageResult(resultCode, data); // 处理裁剪回调 + // 【核心修复】补全requestCode参数,与函数定义一致(3个参数:requestCode、resultCode、data) + handleCropImageResult(requestCode, resultCode, data); // 处理裁剪回调(补全参数) } else if (resultCode != RESULT_OK) { handleOperationCancelOrFail(); // 处理所有取消/失败场景 } @@ -1539,44 +1593,62 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg /** * 处理裁剪回调(REQUEST_CROP_IMAGE) - * 职责:MIUI 0字节文件校验→解析裁剪Bitmap→保存图片→双重刷新预览 + * 适配说明:兼容Java7(无Lambda/Stream,用匿名内部类实现延迟刷新),增强MIUI 0字节文件容错 + * 核心逻辑:校验裁剪文件有效性 → 处理MIUI 0字节文件 → 解析Bitmap → 保存图片 → 双重刷新预览 + * @param requestCode 回调请求码(固定为REQUEST_CROP_IMAGE=2) + * @param resultCode 结果码(RESULT_OK=-1 表示成功) + * @param data 裁剪返回数据(MIUI机型可能为null) */ - 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; + private void handleCropImageResult(int requestCode, int resultCode, Intent data) { + // 1. 裁剪文件基础校验(Java7兼容:显式非空校验,避免空指针) + boolean isFileExist = false; + boolean isFileReadable = false; + long fileSize = 0; + if (_mSourceCropTempFile != null) { + isFileExist = _mSourceCropTempFile.exists(); + isFileReadable = isFileExist ? _mSourceCropTempFile.canRead() : false; + fileSize = isFileExist ? _mSourceCropTempFile.length() : 0; + } + boolean isFileValid = isFileExist && isFileReadable && fileSize > 100; // 大于100字节视为有效 boolean isCropSuccess = (resultCode == RESULT_OK) || isFileValid; - LogUtils.d(TAG, "【裁剪回调】容错校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + ",文件可读=" + isFileReadable + ",文件大小=" + fileSize + " bytes,是否判定为成功=" + isCropSuccess); + // 打印校验日志(Java7原生字符串拼接) + String checkLog = "【裁剪回调】容错校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + + ",文件可读=" + isFileReadable + ",文件大小=" + fileSize + " bytes,是否判定为成功=" + isCropSuccess; + LogUtils.d(TAG, checkLog); // 2. 处理MIUI特有:文件存在但大小为0字节(裁剪工具写入失败) if (isFileExist && fileSize == 0) { - handleMiuiCropZeroByteFile(); + LogUtils.e(TAG, "【裁剪失败】裁剪临时文件为空(MIUI裁剪工具适配问题),建议选择「系统相机裁剪」或第三方裁剪工具"); + ToastUtils.show("裁剪失败,请选择系统相机裁剪重试"); + LogUtils.d(TAG, "【裁剪调试】保留空文件用于排查:" + _mSourceCropTempFile.getPath()); + + // 【Java7兼容:0字节文件重试逻辑】重新初始化临时文件,为下次裁剪做准备 + initCropTempFileAgain(); return; } - // 3. 处理裁剪成功场景 + // 3. 处理裁剪成功场景(resultCode=RESULT_OK 或 文件有效) if (isCropSuccess) { - // 解析裁剪临时文件为Bitmap(适配大图片OOM) + // 解析裁剪临时文件为Bitmap(适配大图片OOM,Java7兼容) Bitmap cropBitmap = parseCropTempFileToBitmap(); if (cropBitmap != null && !cropBitmap.isRecycled()) { - // 保存裁剪图片并双重刷新预览 + // 保存裁剪图片并双重刷新预览(复用原有saveCropBitmap方法,Java7兼容) saveCropBitmap(cropBitmap); doubleRefreshPreview(); LogUtils.d(TAG, "【裁剪完成】裁剪回调处理结束,图片已保存并刷新预览"); } else { ToastUtils.show("获取剪裁图片失败(Bitmap解析异常)"); LogUtils.e(TAG, "【裁剪回调失败】裁剪Bitmap解析失败或已回收"); + // 清理裁剪临时文件(避免残留无效文件) clearCropTempFile(); } } else { - // 处理裁剪取消/失败 + // 4. 处理裁剪取消/失败(resultCode≠RESULT_OK 且 文件无效) handleOperationCancelOrFail(); } } - + /** * 处理所有操作取消/失败(resultCode != RESULT_OK) * 职责:统一提示+清理临时文件,避免残留文件影响下次操作 @@ -1789,7 +1861,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【权限校验】checkAndRequestStoragePermission 触发,Android版本:" + Build.VERSION.SDK_INT); // 核心修复1:Android 14+(API 34+)强制申请 WRITE_EXTERNAL_STORAGE 权限(应用私有外部目录必需) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+ 开始强化,14+ 强制要求 + 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, "【权限校验】Android14+ 应用私有外部目录必需:WRITE_EXTERNAL_STORAGE 权限=" + hasWritePerm); if (!hasWritePerm) { @@ -1842,7 +1914,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【权限刷新】应用私有目录权限已强制设置:" + appPrivateDir.getAbsolutePath() + ",可写=" + appPrivateDir.canWrite() + ",可读=" + appPrivateDir.canRead()); } } - + /** * 递归设置目录及子目录/文件的读写权限(适配Java7 + Android全版本,解决Permission denied问题) * 核心适配:移除Java8+特性(如Lambda、方法引用),兼容Java7语法,同时保障Android14+权限有效性 @@ -1915,38 +1987,63 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.e(TAG, errMsg, e); } } - + /** - * 辅助函数:获取目录类型描述(适配Java7,无Lambda/Stream) - * @param dir 目标目录 - * @return 目录类型描述(兼容低版本语法) + * 辅助函数:获取目录类型描述(适配Java7+Android全版本,精准识别应用内目录) + * 核心特性: + * 1. 兼容Java7语法(无Lambda、Stream、Optional,仅用if-else/显式非空校验) + * 2. 优先识别应用内目录(getFilesDir()),精准匹配裁剪临时文件存储目录 + * 3. 显式处理空指针(所有目录对象先校验非空,避免NullPointerException) + * 4. 清晰区分4类目录(应用内/应用私有外部/缓存/外部存储),便于日志调试 + * @param dir 目标目录(需传入裁剪临时文件/背景文件的父目录) + * @return 目录类型描述(如"应用内目录(getFilesDir(),裁剪临时文件存储目录)") */ private String getDirTypeDesc(File dir) { + // 1. 空目录校验(Java7必需,避免空指针异常) if (dir == null) { - return "未知目录"; + return "未知目录(传入目录为null)"; } - 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 "外部存储目录(需额外权限)"; + // 2. 定义目录路径变量(Java7:提前定义,避免重复调用方法,提升性能) + String dirPath = dir.getAbsolutePath(); + String internalDirPath = ""; // 应用内目录路径(getFilesDir()) + String externalPrivateDirPath = ""; // 应用私有外部目录路径(getExternalFilesDir()) + String cacheDirPath = ""; // 应用缓存目录路径(getCacheDir()) + + // 3. 目录路径赋值(Java7:显式非空校验,避免getFilesDir()等方法返回null导致异常) + File internalDir = getFilesDir(); + if (internalDir != null) { + internalDirPath = internalDir.getAbsolutePath(); + } + + File externalPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (externalPrivateDir != null) { + externalPrivateDirPath = externalPrivateDir.getAbsolutePath(); + } + + File cacheDir = getCacheDir(); + if (cacheDir != null) { + cacheDirPath = cacheDir.getAbsolutePath(); + } + + // 4. 目录类型判断(Java7:用if-else顺序判断,优先识别应用内目录,避免匹配冲突) + // 优先级1:应用内目录(getFilesDir(),裁剪临时文件默认存储目录) + if (!TextUtils.isEmpty(internalDirPath) && dirPath.contains(internalDirPath)) { + return "应用内目录(getFilesDir(),裁剪临时文件存储目录,无需外部权限)"; + } + // 优先级2:应用私有外部目录(getExternalFilesDir(),背景文件存储目录) + else if (!TextUtils.isEmpty(externalPrivateDirPath) && dirPath.contains(externalPrivateDirPath)) { + return "应用私有外部目录(getExternalFilesDir(),背景文件存储目录)"; + } + // 优先级3:应用缓存目录(getCacheDir(),兜底目录) + else if (!TextUtils.isEmpty(cacheDirPath) && dirPath.contains(cacheDirPath)) { + return "应用缓存目录(getCacheDir(),临时文件兜底目录)"; + } + // 优先级4:外部存储目录(需额外权限,功能受限) + else { + return "外部存储目录(非应用私有目录,需额外存储权限,功能可能受限)"; } } - @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { diff --git a/powerbell/src/main/res/xml/file_provider.xml b/powerbell/src/main/res/xml/file_provider.xml index ff896c31..f0547e40 100644 --- a/powerbell/src/main/res/xml/file_provider.xml +++ b/powerbell/src/main/res/xml/file_provider.xml @@ -1,5 +1,14 @@ + + + + + - - - - - - @@ -37,17 +31,24 @@ name="external_cache_path" path="." /> - + + + - - - - - - + + + +