diff --git a/powerbell/build.properties b/powerbell/build.properties index 9a1deffb..3d0a27e9 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Dec 01 07:14:25 GMT 2025 +#Mon Dec 01 07:58:43 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=50 +buildCount=52 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 47a68f28..efb470b4 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 @@ -2,57 +2,51 @@ package cc.winboll.studio.powerbell.activities; import android.Manifest; import android.app.Activity; +import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.ComponentName; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; import android.provider.Settings; import android.text.TextUtils; import android.view.View; -import android.widget.RelativeLayout; import androidx.appcompat.app.AlertDialog; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; -import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog; import cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.App; -import cc.winboll.studio.powerbell.BuildConfig; import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; -import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog; import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; -import cc.winboll.studio.powerbell.utils.FileUtils; import cc.winboll.studio.powerbell.utils.UriUtil; import cc.winboll.studio.powerbell.views.BackgroundView; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; -import java.util.List; import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { public static final String TAG = "BackgroundSettingsActivity"; - public static final int Build_VERSION_CODES_TIRAMISU = 33; // 工具类单例(唯一文件管理入口) private BackgroundSourceUtils mBgSourceUtils; @@ -104,25 +98,25 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 初始化按钮点击事件(仅UI交互,无文件逻辑) initClickListeners(); // 初始化预览 - // 关键:窗口启动时,将正式Bean的内容完整拷贝到预览Bean(覆盖旧预览Bean) - initPreviewBeanFromFormal(); + // 关键:窗口启动时,将正式Bean的内容完整拷贝到预览Bean(覆盖旧预览Bean) + initPreviewBeanFromFormal(); // 处理分享图片意图 handleShareIntent(); - LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(精简版,文件管理依赖BackgroundSourceUtils)"); + LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(Java7 语法版)"); + } + + /** + * 初始化预览Bean:从正式Bean拷贝所有属性(确保每次操作都是正式图的副本) + */ + private void initPreviewBeanFromFormal() { + LogUtils.d(TAG, "【Bean初始化】正式Bean → 预览Bean(拷贝初始化)"); + mBgSourceUtils.setCurrentSourceToPreview(); + // 加载预览Bean中的图片(窗口启动时显示正式图的预览) + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【Bean初始化】预览Bean初始化完成,当前预览路径:" + mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath()); } - /** - * 初始化预览Bean:从正式Bean拷贝所有属性(确保每次操作都是正式图的副本) - */ - private void initPreviewBeanFromFormal() { - LogUtils.d(TAG, "【Bean初始化】正式Bean → 预览Bean(拷贝初始化)"); - mBgSourceUtils.setCurrentSourceToPreview(); - // 加载预览Bean中的图片(窗口启动时显示正式图的预览) - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【Bean初始化】预览Bean初始化完成,当前预览路径:" + mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath()); - } - /** * 初始化工具栏(仅UI逻辑) */ @@ -154,19 +148,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); } - /** - * 初始化预览(调用工具类同步正式背景到预览) - */ -// private void initPreview() { -// mBgSourceUtils.setCurrentSourceToPreview(); -// if (bvPreviewBackground != null) { -// bvPreviewBackground.reloadPreviewBackground(); -// LogUtils.d(TAG, "【初始化】预览视图已加载,BackgroundView 状态:正常"); -// } else { -// LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败"); -// } -// } - /** * 处理分享图片意图(仅UI逻辑,文件处理调用工具类) */ @@ -190,18 +171,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 更新背景预览(调用工具类,无文件操作) + * 更新背景预览(强制使用预览Bean,删除正式Bean分支,Java7语法) */ public void updateBackgroundView(File sourceFile, String sourceFileInfo) { LogUtils.d(TAG, "【预览更新】updateBackgroundView 触发,sourceFile是否为空:" + (sourceFile == null)); - if (sourceFile == null) { - bvPreviewBackground.reloadCurrentBackground(); - LogUtils.d(TAG, "【预览更新】sourceFile为空,加载正式背景"); - } else { - mBgSourceUtils.saveFileToPreviewBean(sourceFile, sourceFileInfo); - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览更新】预览背景更新完成"); + if (sourceFile != null) { + mBgSourceUtils.saveFileToPreviewBean(sourceFile, sourceFileInfo); // 有文件时同步到预览Bean } + // 强制加载预览Bean(无论sourceFile是否为空,全程不使用正式Bean) + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【预览更新】预览背景更新完成(强制使用previewBackgroundBean)"); } // 点击事件:取消背景(仅操作Bean,无文件逻辑) @@ -247,9 +226,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg intents[2] = openDocIntent; } - // 查找有效意图 + // 查找有效意图(Java7 for循环) Intent validIntent = null; - for (Intent intent : intents) { + for (int i = 0; i < intents.length; i++) { + Intent intent = intents[i]; if (intent != null && intent.resolveActivity(getPackageManager()) != null) { validIntent = intent; break; @@ -294,17 +274,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg }; // 点击事件:固定比例裁剪(调用工具类获取裁剪路径,无文件逻辑) - // 点击事件:固定比例裁剪(添加MIUI裁剪提示) - private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能"); - // 从工具类获取正式背景文件,校验有效性 - File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath()); - if (targetFile.exists()) { - // 适配MIUI:弹出裁剪提示(建议选择系统相机) - if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) { - new AlertDialog.Builder(BackgroundSettingsActivity.this) + private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能"); + // 从工具类获取正式背景文件,校验有效性 + File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath()); + if (targetFile.exists()) { + // 适配MIUI:弹出裁剪提示(建议选择系统相机) + if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) { + new AlertDialog.Builder(BackgroundSettingsActivity.this) .setTitle("裁剪提示") .setMessage("若裁剪失败,请选择「系统相机」作为裁剪工具") .setPositiveButton("确定", new DialogInterface.OnClickListener() { @@ -315,29 +294,28 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg }) .setNegativeButton("取消", null) .show(); - } else { - // 非MIUI机型直接启动裁剪 - startCropImageActivity(false); - } - LogUtils.d(TAG, "【裁剪启动】固定比例裁剪已启动"); - } else { - ToastUtils.show("无可用裁剪图片,请先选择/拍照"); - LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片"); - } - } - }; + } else { + // 非MIUI机型直接启动裁剪 + startCropImageActivity(false); + } + LogUtils.d(TAG, "【裁剪启动】固定比例裁剪已启动"); + } else { + ToastUtils.show("无可用裁剪图片,请先选择/拍照"); + LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片"); + } + } + }; // 点击事件:自由裁剪(调用工具类获取裁剪路径,无文件逻辑) - // 点击事件:自由裁剪(添加MIUI裁剪提示) - private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【按钮点击】触发自由裁剪功能"); - File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath()); - if (targetFile.exists()) { - // 适配MIUI:弹出裁剪提示(建议选择系统相机) - if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) { - new AlertDialog.Builder(BackgroundSettingsActivity.this) + private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发自由裁剪功能"); + File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath()); + if (targetFile.exists()) { + // 适配MIUI:弹出裁剪提示(建议选择系统相机) + if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) { + new AlertDialog.Builder(BackgroundSettingsActivity.this) .setTitle("裁剪提示") .setMessage("若裁剪失败,请选择「系统相机」作为裁剪工具") .setPositiveButton("确定", new DialogInterface.OnClickListener() { @@ -348,17 +326,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg }) .setNegativeButton("取消", null) .show(); - } else { - // 非MIUI机型直接启动裁剪 - startCropImageActivity(true); - } - LogUtils.d(TAG, "【裁剪启动】自由裁剪已启动"); - } else { - ToastUtils.show("无可用裁剪图片,请先选择/拍照"); - LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片"); - } - } - }; + } else { + // 非MIUI机型直接启动裁剪 + startCropImageActivity(true); + } + LogUtils.d(TAG, "【裁剪启动】自由裁剪已启动"); + } else { + ToastUtils.show("无可用裁剪图片,请先选择/拍照"); + LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片"); + } + } + }; // 点击事件:拍照(仅权限+相机意图,文件处理调用工具类) private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { @@ -452,218 +430,236 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg }; /** - * 压缩图片并保存(调用工具类复制文件,精简文件操作逻辑) - */ - void compressQualityToRecivedPicture(Bitmap bitmap) { - LogUtils.d(TAG, "【压缩启动】开始压缩图片,Bitmap是否有效:" + (bitmap != null && !bitmap.isRecycled())); - if (bitmap == null || bitmap.isRecycled()) { - ToastUtils.show("压缩失败:图片为空"); - LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收,无法压缩"); - return; - } + * 压缩图片并保存(核心修复:路径非空校验+兜底路径,Java7 手动管理流) + */ + void compressQualityToRecivedPicture(Bitmap bitmap) { + LogUtils.d(TAG, "【压缩启动】开始压缩图片,Bitmap是否有效:" + (bitmap != null && !bitmap.isRecycled())); + if (bitmap == null || bitmap.isRecycled()) { + ToastUtils.show("压缩失败:图片为空"); + LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收,无法压缩"); + return; + } - OutputStream outStream = null; - FileOutputStream fos = null; // 新增:保留FileOutputStream实例引用 - try { - // 从工具类获取预览压缩图路径(统一路径管理) - String scaledCompressFilePath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); - File fRecivedPicture = new File(scaledCompressFilePath); - LogUtils.d(TAG, "【压缩配置】目标压缩路径:" + scaledCompressFilePath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); + OutputStream outStream = null; + FileOutputStream fos = null; + try { + // 核心修复1:获取压缩路径后,增加非空/空字符串校验 + String scaledCompressFilePath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); + if (TextUtils.isEmpty(scaledCompressFilePath)) { + LogUtils.e(TAG, "【压缩异常】工具类返回预览压缩路径为空,使用兜底路径"); + // 核心修复2:兜底路径(复用App临时目录,确保路径有效) + File tempDir = new File(App.getTempDirPath(), "PreviewCompress"); + if (!tempDir.exists()) { + tempDir.mkdirs(); + mBgSourceUtils.copyFile(new File(""), tempDir); // 复用工具类创建目录 + } + // 生成唯一兜底文件名(避免重复) + scaledCompressFilePath = new File(tempDir, "preview_compress_" + System.currentTimeMillis() + ".jpg").getAbsolutePath(); + LogUtils.d(TAG, "【压缩兜底】使用临时路径:" + scaledCompressFilePath); + } - // 确保父目录存在(调用工具类创建目录,避免重复代码) - File parentDir = fRecivedPicture.getParentFile(); - if (!parentDir.exists()) { - parentDir.mkdirs(); - mBgSourceUtils.copyFile(new File(""), parentDir); // 复用工具类目录创建逻辑 - LogUtils.d(TAG, "【压缩准备】目标目录已通过工具类创建:" + parentDir.getAbsolutePath()); - } + File fRecivedPicture = new File(scaledCompressFilePath); + LogUtils.d(TAG, "【压缩配置】目标压缩路径:" + scaledCompressFilePath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); - // 创建目标文件(调用工具类清理旧文件 → 替换为正确的public接口) - if (fRecivedPicture.exists()) { - mBgSourceUtils.clearOldFileByExternal(fRecivedPicture, "旧压缩文件"); // 关键:用新增的public方法 - } - fRecivedPicture.createNewFile(); + // 核心修复3:父目录非空校验,避免NullPointerException + File parentDir = fRecivedPicture.getParentFile(); + if (parentDir == null) { + LogUtils.e(TAG, "【压缩异常】目标文件父目录为空,无法创建"); + ToastUtils.show("压缩失败:路径无效"); + return; + } + // 确保父目录存在(调用工具类创建目录,避免重复代码) + if (!parentDir.exists()) { + parentDir.mkdirs(); + mBgSourceUtils.copyFile(new File(""), parentDir); // 复用工具类目录创建逻辑 + LogUtils.d(TAG, "【压缩准备】目标目录已通过工具类创建:" + parentDir.getAbsolutePath()); + } - // 压缩并保存 → 关键修改:保留FileOutputStream实例 - fos = new FileOutputStream(fRecivedPicture); // 子类实例,支持getFD() - outStream = new BufferedOutputStream(fos); // 包装为缓冲流提升性能 - boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); - outStream.flush(); // 先强制刷新缓冲区到文件流 + // 创建目标文件(调用工具类清理旧文件) + if (fRecivedPicture.exists()) { + mBgSourceUtils.clearOldFileByExternal(fRecivedPicture, "旧压缩文件"); + } + fRecivedPicture.createNewFile(); - // 关键修复:通过FileOutputStream子类实例调用getFD().sync(),确保数据写入磁盘 - if (fos != null) { - try { - fos.getFD().sync(); // 仅子类能调用,强制将文件流同步到物理磁盘 - LogUtils.d(TAG, "【压缩保存】已强制同步数据到磁盘,确保文件写入完成"); - } catch (IOException e) { - // 兼容异常:部分设备/Android版本可能不支持sync(),用flush()兜底 - LogUtils.w(TAG, "【压缩保存】getFD().sync()调用失败,已用flush()兜底:" + e.getMessage()); - outStream.flush(); // 双重兜底,确保数据不丢失 - } - } + // 压缩并保存(Java7 手动管理流) + fos = new FileOutputStream(fRecivedPicture); + outStream = new BufferedOutputStream(fos); + boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); + outStream.flush(); - LogUtils.d(TAG, "【压缩结果】图片压缩:" + (compressSuccess ? "成功" : "失败") + ",压缩后大小:" + fRecivedPicture.length() / 1024 + "KB"); + // 强制同步数据到磁盘(兼容部分设备) + if (fos != null) { + try { + fos.getFD().sync(); + LogUtils.d(TAG, "【压缩保存】已强制同步数据到磁盘"); + } catch (IOException e) { + LogUtils.w(TAG, "【压缩保存】sync()调用失败,用flush()兜底:" + e.getMessage()); + outStream.flush(); + } + } - if (!compressSuccess) { - ToastUtils.show("图片压缩失败"); - LogUtils.e(TAG, "【压缩失败】Bitmap压缩返回false"); - } - } catch (IOException e) { - LogUtils.e(TAG, "【压缩异常】IO异常:" + e.getMessage()); - ToastUtils.show("图片压缩失败"); - } finally { - // 关闭流资源 → 先关缓冲流,再关文件流(规范操作) - if (outStream != null) { - try { - outStream.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【压缩异常】缓冲流关闭失败:" + e.getMessage()); - } - } - if (fos != null) { // 新增:关闭FileOutputStream - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【压缩异常】文件流关闭失败:" + e.getMessage()); - } - } - // 回收Bitmap - if (bitmap != null && !bitmap.isRecycled()) { - bitmap.recycle(); - } - } - } + LogUtils.d(TAG, "【压缩结果】图片压缩:" + (compressSuccess ? "成功" : "失败") + ",压缩后大小:" + fRecivedPicture.length() / 1024 + "KB"); + + if (!compressSuccess) { + ToastUtils.show("图片压缩失败"); + LogUtils.e(TAG, "【压缩失败】Bitmap压缩返回false"); + } + } catch (IOException e) { + LogUtils.e(TAG, "【压缩异常】IO异常:" + e.getMessage()); + ToastUtils.show("图片压缩失败"); + } finally { + // 关闭流资源(Java7 手动关闭,避免内存泄漏) + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【压缩异常】缓冲流关闭失败:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【压缩异常】文件流关闭失败:" + e.getMessage()); + } + } + // 回收Bitmap + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + } /** - * 启动图片裁剪活动(核心:调用工具类获取裁剪路径,精简文件管理逻辑) + * 启动图片裁剪活动(核心:调用工具类获取裁剪路径,Java7 语法) * @param isCropFree 是否自由裁剪 */ public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); - BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - previewBean.setIsUseBackgroundScaledCompressFile(true); - mBgSourceUtils.saveSettings(); + LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setIsUseBackgroundScaledCompressFile(true); + mBgSourceUtils.saveSettings(); - // 1. 预览图片有效性校验(保留强化校验逻辑) - String previewFilePath = mBgSourceUtils.getPreviewBackgroundFilePath(); - if (TextUtils.isEmpty(previewFilePath)) { - ToastUtils.show("预览图片路径为空"); - LogUtils.e(TAG, "【裁剪失败】预览图片路径为空"); - return; - } - File previewFile = new File(previewFilePath); - LogUtils.d(TAG, "【裁剪校验】预览图片状态:路径=" + previewFile.getAbsolutePath() + ",是否存在=" + previewFile.exists() + ",是否为文件=" + previewFile.isFile() + ",大小=" + (previewFile.exists() ? previewFile.length() : 0) + "bytes"); - if (!previewFile.exists() || !previewFile.isFile() || previewFile.length() <= 100) { - ToastUtils.show("预览图片不存在或损坏"); - LogUtils.e(TAG, "【裁剪失败】预览图片无效"); - return; - } + // 1. 预览图片有效性校验(保留强化校验逻辑) + String previewFilePath = mBgSourceUtils.getPreviewBackgroundFilePath(); + if (TextUtils.isEmpty(previewFilePath)) { + ToastUtils.show("预览图片路径为空"); + LogUtils.e(TAG, "【裁剪失败】预览图片路径为空"); + return; + } + File previewFile = new File(previewFilePath); + LogUtils.d(TAG, "【裁剪校验】预览图片状态:路径=" + previewFile.getAbsolutePath() + ",是否存在=" + previewFile.exists() + ",是否为文件=" + previewFile.isFile() + ",大小=" + (previewFile.exists() ? previewFile.length() : 0) + "bytes"); + if (!previewFile.exists() || !previewFile.isFile() || previewFile.length() <= 100) { + ToastUtils.show("预览图片不存在或损坏"); + LogUtils.e(TAG, "【裁剪失败】预览图片无效"); + return; + } - // 2. 生成裁剪输入Uri(强化MIUI权限授予) - Uri inputUri = null; - try { - inputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), previewFile); - // 显式授予MIUI裁剪工具读写权限(关键适配) - grantUriPermission("com.miui.gallery", inputUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - LogUtils.d(TAG, "【裁剪Uri】输入Uri生成成功 : " + inputUri.toString() + ",已授予MIUI裁剪工具权限"); - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败:" + e.getMessage(), e); - ToastUtils.show("图片裁剪失败:无法获取图片权限"); - mBgSourceUtils.clearCropTempFiles(); - return; - } + // 2. 生成裁剪输入Uri(强化MIUI权限授予) + Uri inputUri = null; + try { + inputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), previewFile); + // 显式授予MIUI裁剪工具读写权限(关键适配) + grantUriPermission("com.miui.gallery", inputUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + LogUtils.d(TAG, "【裁剪Uri】输入Uri生成成功 : " + inputUri.toString() + ",已授予MIUI裁剪工具权限"); + } catch (Exception e) { + LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败:" + e.getMessage(), e); + ToastUtils.show("图片裁剪失败:无法获取图片权限"); + mBgSourceUtils.clearCropTempFiles(); + return; + } - // 3. 调用工具类创建裁剪路径(原有逻辑不变) - File cropTempFile = mBgSourceUtils.createCropFileProviderPath(); - if (cropTempFile == null) { - ToastUtils.show("裁剪路径创建失败,请重试"); - return; - } + // 3. 调用工具类创建裁剪路径(原有逻辑不变) + File cropTempFile = mBgSourceUtils.createCropFileProviderPath(); + if (cropTempFile == null) { + ToastUtils.show("裁剪路径创建失败,请重试"); + return; + } - // 4. 构建裁剪意图(适配MIUI核心修改) - Intent intent = new Intent("com.android.camera.action.CROP"); - // 恢复inputUri和类型,移除setDataAndType(null, null)(MIUI必需) - intent.setDataAndType(inputUri, "image/*"); - intent.putExtra("crop", "true"); - intent.putExtra("noFaceDetection", true); + // 4. 构建裁剪意图(适配MIUI核心修改) + Intent intent = new Intent("com.android.camera.action.CROP"); + // 恢复inputUri和类型,移除setDataAndType(null, null)(MIUI必需) + intent.setDataAndType(inputUri, "image/*"); + intent.putExtra("crop", "true"); + intent.putExtra("noFaceDetection", true); - // 裁剪比例设置(原有逻辑不变) - if (!isCropFree) { - int viewWidth = bvPreviewBackground.getWidth() > 0 ? bvPreviewBackground.getWidth() : getResources().getDisplayMetrics().widthPixels; - int viewHeight = bvPreviewBackground.getHeight() > 0 ? bvPreviewBackground.getHeight() : getResources().getDisplayMetrics().heightPixels; - int gcd = calculateGCD(viewWidth, viewHeight); - intent.putExtra("aspectX", viewWidth / gcd); - intent.putExtra("aspectY", viewHeight / gcd); - } else { - intent.putExtra("aspectX", 1); - intent.putExtra("aspectY", 1); - } + // 裁剪比例设置(原有逻辑不变) + if (!isCropFree) { + int viewWidth = bvPreviewBackground.getWidth() > 0 ? bvPreviewBackground.getWidth() : getResources().getDisplayMetrics().widthPixels; + int viewHeight = bvPreviewBackground.getHeight() > 0 ? bvPreviewBackground.getHeight() : getResources().getDisplayMetrics().heightPixels; + int gcd = calculateGCD(viewWidth, viewHeight); + intent.putExtra("aspectX", viewWidth / gcd); + intent.putExtra("aspectY", viewHeight / gcd); + } else { + intent.putExtra("aspectX", 1); + intent.putExtra("aspectY", 1); + } - // 输出尺寸设置(原有逻辑不变) - int maxOutputSize = Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU ? 1536 : 2048; - int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize); - int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize); - intent.putExtra("outputX", outputX); - intent.putExtra("outputY", outputY); - intent.putExtra("scale", true); - intent.putExtra("scaleUpIfNeeded", true); + // 输出尺寸设置(原有逻辑不变) + int maxOutputSize = 2048; // 移除Android13+判断,统一用2048(适配所有版本) + int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize); + int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize); + intent.putExtra("outputX", outputX); + intent.putExtra("outputY", outputY); + intent.putExtra("scale", true); + intent.putExtra("scaleUpIfNeeded", true); - // 输出配置(指定JPEG格式,适配MIUI) - intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); - intent.putExtra("quality", 80); - intent.putExtra("return-data", false); // 禁用返回Bitmap,避免OOM + // 输出配置(指定JPEG格式,适配MIUI) + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + intent.putExtra("quality", 80); + intent.putExtra("return-data", false); // 禁用返回Bitmap,避免OOM - // 权限Flags(添加持久化权限,适配MIUI) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } + // 权限Flags(添加持久化权限,适配MIUI) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } - // 5. 适配系统裁剪工具(MIUI专属处理) - try { - List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (!resolveInfos.isEmpty()) { - ResolveInfo resolveInfo = resolveInfos.get(0); - String cropPackageName = resolveInfo.activityInfo.packageName; - String cropActivityName = resolveInfo.activityInfo.name; - LogUtils.d(TAG, "【裁剪适配】找到裁剪工具:包名=" + cropPackageName + ",Activity=" + cropActivityName); + // 5. 适配系统裁剪工具(MIUI专属处理,Java7 for循环) + try { + List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (!resolveInfos.isEmpty()) { + ResolveInfo resolveInfo = resolveInfos.get(0); + String cropPackageName = resolveInfo.activityInfo.packageName; + String cropActivityName = resolveInfo.activityInfo.name; + LogUtils.d(TAG, "【裁剪适配】找到裁剪工具:包名=" + cropPackageName + ",Activity=" + cropActivityName); - // 生成输出Uri(显式授予MIUI写入权限) - Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile); - // 适配MIUI裁剪工具,额外授予持久化写入权限 - if (cropPackageName.equals("com.miui.gallery")) { - grantUriPermission(cropPackageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - LogUtils.d(TAG, "【裁剪适配】已授予MIUI裁剪工具输出Uri写入权限"); - } + // 生成输出Uri(显式授予MIUI写入权限) + Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile); + // 适配MIUI裁剪工具,额外授予持久化写入权限 + if (cropPackageName.equals("com.miui.gallery")) { + grantUriPermission(cropPackageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + LogUtils.d(TAG, "【裁剪适配】已授予MIUI裁剪工具输出Uri写入权限"); + } - // 显式设置Component(移除setDataAndType(null, null),避免参数冲突) - Intent cropIntent = new Intent(intent); - cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); - cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); - cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + // 显式设置Component(移除setDataAndType(null, null),避免参数冲突) + Intent cropIntent = new Intent(intent); + cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); + cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); - LogUtils.d(TAG, "【裁剪启动】已启动系统裁剪工具,输出路径:" + cropTempFile.getAbsolutePath()); - } else { - // 兜底:启动第三方裁剪工具(原有逻辑不变) - Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile); - intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); - Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); - chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (chooser.resolveActivity(getPackageManager()) != null) { - startActivityForResult(chooser, REQUEST_CROP_IMAGE); - } else { - ToastUtils.show("无可用裁剪工具,请安装系统相机"); - mBgSourceUtils.clearCropTempFiles(); - } - } - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e); - ToastUtils.show("无法启动裁剪工具"); - mBgSourceUtils.clearCropTempFiles(); - } - } + startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); + LogUtils.d(TAG, "【裁剪启动】已启动系统裁剪工具,输出路径:" + cropTempFile.getAbsolutePath()); + } else { + // 兜底:启动第三方裁剪工具(原有逻辑不变) + Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (chooser.resolveActivity(getPackageManager()) != null) { + startActivityForResult(chooser, REQUEST_CROP_IMAGE); + } else { + ToastUtils.show("无可用裁剪工具,请安装系统相机"); + mBgSourceUtils.clearCropTempFiles(); + } + } + } catch (Exception e) { + LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e); + ToastUtils.show("无法启动裁剪工具"); + mBgSourceUtils.clearCropTempFiles(); + } + } /** * 计算最大公约数(简化裁剪比例) @@ -770,744 +766,594 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * 处理裁剪回调(调用工具类获取裁剪文件,精简文件操作) */ private void handleCropImageResult(int requestCode, int resultCode, Intent data) { - // 从工具类获取裁剪临时文件(唯一入口) - File cropTempFile = mBgSourceUtils.getCropTempFile(); - boolean isFileExist = cropTempFile != null && cropTempFile.exists(); - boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; - long fileSize = isFileExist ? cropTempFile.length() : 0; - // 适配MIUI:仅resultCode=RESULT_OK且文件有效时视为成功(resultCode=0视为取消) - boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100; + // 从工具类获取裁剪临时文件(唯一入口) + File cropTempFile = mBgSourceUtils.getCropTempFile(); + boolean isFileExist = cropTempFile != null && cropTempFile.exists(); + boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; + long fileSize = isFileExist ? cropTempFile.length() : 0; + // 适配MIUI:仅resultCode=RESULT_OK且文件有效时视为成功(resultCode=0视为取消) + boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100; - // 打印校验日志 - LogUtils.d(TAG, "【裁剪回调】校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + ",大小=" + fileSize + "bytes,是否成功=" + isCropSuccess); + // 打印校验日志 + LogUtils.d(TAG, "【裁剪回调】校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + ",大小=" + fileSize + "bytes,是否成功=" + isCropSuccess); - // 处理MIUI裁剪取消(resultCode=0视为取消,而非失败) - if (resultCode == 0 && !isCropSuccess) { - LogUtils.d(TAG, "【裁剪回调】MIUI 裁剪工具已取消"); - ToastUtils.show("裁剪已取消"); - mBgSourceUtils.clearCropTempFiles(); - return; - } + // 处理MIUI裁剪取消(resultCode=0视为取消,而非失败) + if (resultCode == 0 && !isCropSuccess) { + LogUtils.d(TAG, "【裁剪回调】MIUI 裁剪工具已取消"); + ToastUtils.show("裁剪已取消"); + mBgSourceUtils.clearCropTempFiles(); + return; + } - // 处理裁剪文件为空(真正的裁剪失败) - if (isFileExist && fileSize == 0) { - LogUtils.e(TAG, "【裁剪失败】裁剪文件为空(MIUI适配问题)"); - ToastUtils.show("裁剪失败,请尝试选择「系统相机」裁剪或更换图片"); - mBgSourceUtils.clearCropTempFiles(); - return; - } + // 处理裁剪文件为空(真正的裁剪失败) + if (isFileExist && fileSize == 0) { + LogUtils.e(TAG, "【裁剪失败】裁剪文件为空(MIUI适配问题)"); + ToastUtils.show("裁剪失败,请尝试选择「系统相机」裁剪或更换图片"); + mBgSourceUtils.clearCropTempFiles(); + return; + } - // 裁剪成功:解析Bitmap并保存(原有逻辑不变) - if (isCropSuccess) { - Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); - if (cropBitmap != null && !cropBitmap.isRecycled()) { - saveCropBitmap(cropBitmap); // 保存裁剪结果 - doubleRefreshPreview(); // 双重刷新预览(适配MIUI渲染延迟) - LogUtils.d(TAG, "【裁剪完成】裁剪回调处理结束"); - } else { - ToastUtils.show("获取裁剪图片失败"); - LogUtils.e(TAG, "【裁剪回调失败】Bitmap解析异常"); - mBgSourceUtils.clearCropTempFiles(); - } - } else { - // 其他失败场景(统一处理) - handleOperationCancelOrFail(); - } - } + // 裁剪成功:解析Bitmap并保存(原有逻辑不变) + if (isCropSuccess) { + Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); + if (cropBitmap != null && !cropBitmap.isRecycled()) { + saveCropBitmap(cropBitmap); // 保存裁剪结果 + doubleRefreshPreview(); // 双重刷新预览(适配MIUI渲染延迟) + LogUtils.d(TAG, "【裁剪完成】裁剪回调处理结束"); + } else { + ToastUtils.show("获取裁剪图片失败"); + LogUtils.e(TAG, "【裁剪回调失败】Bitmap解析异常"); + mBgSourceUtils.clearCropTempFiles(); + } + } else { + // 其他失败场景(统一处理) + handleOperationCancelOrFail(); + } + } - /** - * 处理所有操作取消/失败(统一清理+提示,无文件逻辑) - */ - private void handleOperationCancelOrFail() { - LogUtils.d(TAG, "【操作回调】操作取消或失败"); - ToastUtils.show("操作已取消"); - mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理裁剪临时文件 - } + /** + * 处理所有操作取消/失败(统一清理+提示,无文件逻辑) + */ + private void handleOperationCancelOrFail() { + LogUtils.d(TAG, "【操作回调】操作取消或失败"); + ToastUtils.show("操作已取消"); + mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理裁剪临时文件 + } - /** - * 辅助函数:为选图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添加持久化读取权限(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为File(核心修复:适配Android11+ 共享存储私有路径) - * 放弃直接路径读取,改用ContentResolver流复制,避免Permission denied - */ - /** - * 辅助函数:解析选图Uri为File(核心修复:Android14+ 共享存储私有路径适配) - * 放弃直接路径读取,改用ContentResolver流复制,避免Permission denied - */ - private File parseUriToFile(Uri uri) { - File targetFile = null; - // 1. 尝试解析路径(兼容旧版本/普通路径) - String filePath = UriUtil.getFilePathFromUri(this, uri); - LogUtils.d(TAG, "【选图解析】Uri解析路径:" + filePath); + /** + * 辅助函数:解析选图Uri为File(核心修复:Android14+ 共享存储私有路径适配) + * 放弃直接路径读取,改用ContentResolver流复制,避免Permission denied + */ + private File parseUriToFile(Uri uri) { + File targetFile = null; + // 1. 尝试解析路径(兼容旧版本/普通路径) + String filePath = UriUtil.getFilePathFromUri(this, uri); + LogUtils.d(TAG, "【选图解析】Uri解析路径:" + filePath); - // 2. 路径有效且可读取(兼容Android 10- 或 非隐藏路径) - if (!TextUtils.isEmpty(filePath)) { - File tempFile = new File(filePath); - // 双重校验:文件存在 + 实际可读取(避免canRead()假阳性) - if (isFileActuallyReadable(tempFile)) { - targetFile = tempFile; - } else { - // 路径存在但无权限 → 流复制兜底(核心修复) - targetFile = createTempFileByStreamCopy(uri); - } - } else { - // 3. 路径解析失败(ContentProvider Uri)→ 流复制兜底 - targetFile = createTempFileByStreamCopy(uri); - } + // 2. 路径有效且可读取(兼容Android 10- 或 非隐藏路径) + if (!TextUtils.isEmpty(filePath)) { + File tempFile = new File(filePath); + // 双重校验:文件存在 + 实际可读取(避免canRead()假阳性) + if (isFileActuallyReadable(tempFile)) { + targetFile = tempFile; + } else { + // 路径存在但无权限 → 流复制兜底(核心修复) + targetFile = createTempFileByStreamCopy(uri); + } + } else { + // 3. 路径解析失败(ContentProvider Uri)→ 流复制兜底 + targetFile = createTempFileByStreamCopy(uri); + } - // 4. 校验目标文件有效性(避免后续逻辑崩溃) - if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { - LogUtils.e(TAG, "【选图解析】生成的目标文件无效:" + (targetFile != null ? targetFile.getAbsolutePath() : "null")); - ToastUtils.show("图片读取失败,请重新选择"); - return null; - } - LogUtils.d(TAG, "【选图解析】Uri解析成功,目标文件:" + targetFile.getAbsolutePath() + ",大小:" + targetFile.length() + "bytes"); - return targetFile; - } + // 4. 校验目标文件有效性(避免后续逻辑崩溃) + if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { + LogUtils.e(TAG, "【选图解析】生成的目标文件无效:" + (targetFile != null ? targetFile.getAbsolutePath() : "null")); + ToastUtils.show("图片读取失败,请重新选择"); + return null; + } + LogUtils.d(TAG, "【选图解析】Uri解析成功,目标文件:" + targetFile.getAbsolutePath() + ",大小:" + targetFile.length() + "bytes"); + return targetFile; + } - /** - * 辅助函数:通过ContentResolver流复制生成临时文件(核心修复:绕开共享存储权限限制) - * 直接读取Uri流,不依赖文件路径,适配所有相册Uri(包括私有隐藏路径) - */ - private File createTempFileByStreamCopy(Uri uri) { - // 1. 初始化临时目录(复用工具类目录,统一路径管理) - File tempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp"); - if (!tempDir.exists()) { - mBgSourceUtils.copyFile(new File(""), tempDir); // 复用工具类目录创建逻辑 - } + /** + * 辅助函数:通过ContentResolver流复制生成临时文件(核心修复:绕开共享存储权限限制) + * 直接读取Uri流,不依赖文件路径,适配所有相册Uri(包括私有隐藏路径),Java7语法 + */ + private File createTempFileByStreamCopy(Uri uri) { + // 1. 初始化临时目录(复用工具类目录,统一路径管理) + File tempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp"); + if (!tempDir.exists()) { + mBgSourceUtils.copyFile(new File(""), tempDir); // 复用工具类目录创建逻辑 + } - // 2. 生成唯一临时文件名(避免重复) - String uniqueFileName = "selected_temp_" + System.currentTimeMillis() + ".jpg"; - File tempFile = new File(tempDir, uniqueFileName); + // 2. 生成唯一临时文件名(避免重复) + String uniqueFileName = "selected_temp_" + System.currentTimeMillis() + ".jpg"; + File tempFile = new File(tempDir, uniqueFileName); - // 3. 流复制(核心:用ContentResolver打开Uri流,绕开路径权限限制) - InputStream is = null; - FileOutputStream fos = null; - try { - // 清理旧文件 - if (tempFile.exists()) { - mBgSourceUtils.clearOldFileByExternal(tempFile, "旧选图临时文件"); - } + // 3. 流复制(核心:用ContentResolver打开Uri流,绕开路径权限限制,Java7手动关闭流) + InputStream is = null; + FileOutputStream fos = null; + try { + // 清理旧文件 + if (tempFile.exists()) { + mBgSourceUtils.clearOldFileByExternal(tempFile, "旧选图临时文件"); + } - // 打开Uri输入流(关键:Android14+ 仅允许通过ContentResolver读取共享存储私有文件) - is = getContentResolver().openInputStream(uri); - if (is == null) { - LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败:" + uri.toString()); - return null; - } + // 打开Uri输入流(关键:Android14+ 仅允许通过ContentResolver读取共享存储私有文件) + is = getContentResolver().openInputStream(uri); + if (is == null) { + LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败:" + uri.toString()); + return null; + } - // 打开目标文件输出流 - fos = new FileOutputStream(tempFile); - byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,提升复制效率 - int len; - // 循环读取流并写入目标文件 - while ((len = is.read(buffer)) != -1) { - fos.write(buffer, 0, len); - } - fos.flush(); - fos.getFD().sync(); // 确保数据写入磁盘(避免缓冲导致文件损坏) + // 打开目标文件输出流 + fos = new FileOutputStream(tempFile); + byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,提升复制效率 + int len; + // 循环读取流并写入目标文件(Java7 while循环) + while ((len = is.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + fos.flush(); + fos.getFD().sync(); // 确保数据写入磁盘(避免缓冲导致文件损坏) - LogUtils.d(TAG, "【选图解析】流复制成功:" + tempFile.getAbsolutePath() + ",大小:" + tempFile.length() + "bytes"); - } catch (Exception e) { - LogUtils.e(TAG, "【选图解析】流复制失败:" + e.getMessage(), e); - tempFile = null; - ToastUtils.show("图片读取失败,请重新选择"); - } finally { - // 关闭流资源(Java7 手动关闭,避免内存泄漏) - if (is != null) { - try { - is.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【选图解析】输入流关闭失败:" + e.getMessage()); - } - } - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【选图解析】输出流关闭失败:" + e.getMessage()); - } - } - } - return tempFile; - } + LogUtils.d(TAG, "【选图解析】流复制成功:" + tempFile.getAbsolutePath() + ",大小:" + tempFile.length() + "bytes"); + } catch (Exception e) { + LogUtils.e(TAG, "【选图解析】流复制失败:" + e.getMessage(), e); + tempFile = null; + ToastUtils.show("图片读取失败,请重新选择"); + } finally { + // 关闭流资源(Java7 手动关闭,避免内存泄漏) + if (is != null) { + try { + is.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【选图解析】输入流关闭失败:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【选图解析】输出流关闭失败:" + e.getMessage()); + } + } + } + return tempFile; + } - /** - * 辅助函数:校验文件是否实际可读取(解决Android14+ canRead()假阳性问题) - */ - private boolean isFileActuallyReadable(File file) { - if (file == null || !file.exists() || !file.isFile()) { - return false; - } - // 实际尝试读取文件(避免路径存在但无权限) - FileInputStream fis = null; - try { - fis = new FileInputStream(file); - fis.read(new byte[1]); // 读取1字节验证权限 - return true; - } catch (Exception e) { - LogUtils.w(TAG, "【选图解析】文件存在但无读取权限:" + file.getAbsolutePath()); - return false; - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【选图解析】校验流关闭失败:" + e.getMessage()); - } - } - } - } + /** + * 辅助函数:校验文件是否实际可读取(解决Android14+ canRead()假阳性问题) + */ + private boolean isFileActuallyReadable(File file) { + if (file == null || !file.exists() || !file.isFile()) { + return false; + } + // 实际尝试读取文件(避免路径存在但无权限) + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + fis.read(new byte[1]); // 读取1字节验证权限 + return true; + } catch (Exception e) { + LogUtils.w(TAG, "【选图解析】文件存在但无读取权限:" + file.getAbsolutePath()); + return false; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【选图解析】校验流关闭失败:" + e.getMessage()); + } + } + } + } - /** - * 处理选图回调(新增:目标文件有效性校验,避免后续逻辑崩溃) - */ - private void handleSelectPictureResult(int resultCode, Intent data) { - if (resultCode != RESULT_OK || data == null) { - handleOperationCancelOrFail(); - return; - } + /** + * 处理选图回调(新增:目标文件有效性校验,避免后续逻辑崩溃) + */ + private void handleSelectPictureResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK || data == null) { + handleOperationCancelOrFail(); + return; + } - Uri selectedImage = data.getData(); - if (selectedImage == null) { - ToastUtils.show("选择的图片Uri为空"); - mBgSourceUtils.clearCropTempFiles(); - return; - } - LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString()); + Uri selectedImage = data.getData(); + if (selectedImage == null) { + ToastUtils.show("选择的图片Uri为空"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString()); - // 授予持久化权限(Android4.4+) - grantPersistableUriPermission(selectedImage); + // 授予持久化权限(Android4.4+) + grantPersistableUriPermission(selectedImage); - // 解析Uri为文件(核心:使用修复后的流复制方法) - File selectedFile = parseUriToFile(selectedImage); - if (selectedFile == null || !selectedFile.exists() || selectedFile.length() <= 0) { - ToastUtils.show("选择的图片文件无效"); - mBgSourceUtils.clearCropTempFiles(); - return; - } + // 解析Uri为文件(核心:使用修复后的流复制方法) + File selectedFile = parseUriToFile(selectedImage); + if (selectedFile == null || !selectedFile.exists() || selectedFile.length() <= 0) { + ToastUtils.show("选择的图片文件无效"); + mBgSourceUtils.clearCropTempFiles(); + return; + } - // 关键修复:选图后生成压缩图(确保预览时压缩图存在) - LogUtils.d(TAG, "【选图压缩】开始生成预览压缩图"); - Bitmap selectBitmap = BitmapFactory.decodeFile(selectedFile.getAbsolutePath()); - if (selectBitmap != null && !selectBitmap.isRecycled()) { - compressQualityToRecivedPicture(selectBitmap); // 生成压缩图 - selectBitmap.recycle(); // 回收Bitmap,避免OOM - } else { - ToastUtils.show("选图后压缩图生成失败"); - LogUtils.e(TAG, "【选图压缩失败】无法解析选图文件为Bitmap"); - mBgSourceUtils.clearCropTempFiles(); - return; - } + // 关键修复:选图后生成压缩图(确保预览时压缩图存在) + LogUtils.d(TAG, "【选图压缩】开始生成预览压缩图"); + Bitmap selectBitmap = BitmapFactory.decodeFile(selectedFile.getAbsolutePath()); + if (selectBitmap != null && !selectBitmap.isRecycled()) { + compressQualityToRecivedPicture(selectBitmap); // 生成压缩图 + selectBitmap.recycle(); // 回收Bitmap,避免OOM + } else { + ToastUtils.show("选图后压缩图生成失败"); + LogUtils.e(TAG, "【选图压缩失败】无法解析选图文件为Bitmap"); + mBgSourceUtils.clearCropTempFiles(); + return; + } - // 同步到预览并启动裁剪(此时正式图和压缩图均已存在) - mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString()); - bvPreviewBackground.reloadPreviewBackground(); - startCropImageActivity(false); - LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪(正式图+压缩图均已生成)"); - } + // 同步到预览并启动裁剪(此时正式图和压缩图均已存在) + mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString()); + bvPreviewBackground.reloadPreviewBackground(); + startCropImageActivity(false); + LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪(正式图+压缩图均已生成)"); + } - /** - * 辅助函数:从拍照数据中获取Bitmap(精简版,无文件操作) - */ - private Bitmap getTakePhotoBitmap(Intent data) { - Bundle extras = data.getExtras(); - if (extras == null) { - return null; - } - Bitmap bitmap = (Bitmap) extras.get("data"); - LogUtils.d(TAG, "【拍照回调】获取Bitmap:" + (bitmap != null ? "成功" : "失败")); - return bitmap; - } + /** + * 辅助函数:从拍照数据中获取Bitmap(精简版,无文件操作) + */ + private Bitmap getTakePhotoBitmap(Intent data) { + Bundle extras = data.getExtras(); + if (extras == null) { + return null; + } + Bitmap bitmap = (Bitmap) extras.get("data"); + LogUtils.d(TAG, "【拍照回调】获取Bitmap:" + (bitmap != null ? "成功" : "失败")); + return bitmap; + } - /** - * 辅助函数:解析裁剪临时文件为Bitmap(适配大图片OOM) - */ - private Bitmap parseCropTempFileToBitmap(File cropTempFile) { - if (cropTempFile == null || !cropTempFile.exists() || cropTempFile.length() <= 100) { - return null; - } + /** + * 辅助函数:解析裁剪临时文件为Bitmap(适配大图片OOM) + */ + private Bitmap parseCropTempFileToBitmap(File cropTempFile) { + if (cropTempFile == null || !cropTempFile.exists() || cropTempFile.length() <= 100) { + return null; + } - Bitmap cropBitmap = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - // 第一步:仅获取图片信息,不加载Bitmap - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(cropTempFile.getPath(), options); - LogUtils.d(TAG, "【Bitmap解析】图片宽高:" + options.outWidth + "x" + options.outHeight); + Bitmap cropBitmap = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + // 第一步:仅获取图片信息,不加载Bitmap + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(cropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】图片宽高:" + options.outWidth + "x" + options.outHeight); - // 自动适配格式+计算采样率 - options.inPreferredConfig = getBitmapConfigByMimeType(options.outMimeType); - options.inSampleSize = calculateBitmapSampleRate(options, 2048); + // 自动适配格式+计算采样率 + options.inPreferredConfig = getBitmapConfigByMimeType(options.outMimeType); + options.inSampleSize = calculateBitmapSampleRate(options, 2048); - // 第二步:正式加载Bitmap - options.inJustDecodeBounds = false; - cropBitmap = BitmapFactory.decodeFile(cropTempFile.getPath(), options); - LogUtils.d(TAG, "【Bitmap解析】加载Bitmap:" + (cropBitmap != null ? "成功" : "失败")); - } catch (Exception e) { - LogUtils.e(TAG, "【Bitmap解析】解析失败:" + e.getMessage(), e); - } - return cropBitmap; - } + // 第二步:正式加载Bitmap + options.inJustDecodeBounds = false; + cropBitmap = BitmapFactory.decodeFile(cropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】加载Bitmap:" + (cropBitmap != null ? "成功" : "失败")); + } catch (Exception e) { + LogUtils.e(TAG, "【Bitmap解析】解析失败:" + e.getMessage(), e); + } + return cropBitmap; + } - /** - * 辅助函数:根据图片格式适配Bitmap配置 - */ - private Bitmap.Config getBitmapConfigByMimeType(String mimeType) { - return (mimeType != null && mimeType.contains("png")) - ? Bitmap.Config.ARGB_8888 - : Bitmap.Config.RGB_565; - } + /** + * 辅助函数:根据图片格式适配Bitmap配置 + */ + private Bitmap.Config getBitmapConfigByMimeType(String mimeType) { + if (mimeType != null && mimeType.contains("png")) { + return Bitmap.Config.ARGB_8888; + } else { + return 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); - return sampleRate; - } + /** + * 辅助函数:计算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); + return sampleRate; + } - /** - * 保存裁剪后的Bitmap(调用工具类保存,精简文件逻辑) - */ - private void saveCropBitmap(Bitmap bitmap) { - LogUtils.d(TAG, "【保存启动】开始保存裁剪图片(仅更新预览Bean,不影响正式Bean)"); - if (bitmap == null || bitmap.isRecycled()) { - ToastUtils.show("裁剪图片为空"); - mBgSourceUtils.clearCropTempFiles(); - return; - } + /** + * 保存裁剪后的Bitmap(调用工具类保存,Java7 语法) + */ + private void saveCropBitmap(Bitmap bitmap) { + LogUtils.d(TAG, "【保存启动】开始保存裁剪图片(仅更新预览Bean,不影响正式Bean)"); + if (bitmap == null || bitmap.isRecycled()) { + ToastUtils.show("裁剪图片为空"); + mBgSourceUtils.clearCropTempFiles(); + return; + } - // 内存优化:大图片缩放(保留原有逻辑) - Bitmap scaledBitmap = bitmap; - int originalSize = bitmap.getByteCount() / 1024 / 1024; // 转换为MB - if (originalSize > 5) { // 超过5MB自动缩放 - float scale = 1.0f; - while (scaledBitmap.getByteCount() / 1024 / 1024 > 2) { // 缩放至2MB以内 - scale -= 0.2f; - if (scale < 0.2f) break; - scaledBitmap = scaleBitmap(scaledBitmap, scale); - } - if (scaledBitmap != bitmap) { - bitmap.recycle(); // 回收原Bitmap - } - } + // 内存优化:大图片缩放(保留原有逻辑,Java7 语法) + Bitmap scaledBitmap = bitmap; + int originalSize = bitmap.getByteCount() / 1024 / 1024; // 转换为MB + if (originalSize > 5) { // 超过5MB自动缩放 + float scale = 1.0f; + while (scaledBitmap.getByteCount() / 1024 / 1024 > 2) { // 缩放至2MB以内 + scale -= 0.2f; + if (scale < 0.2f) break; + scaledBitmap = scaleBitmap(scaledBitmap, scale); + } + LogUtils.d(TAG, "【内存优化】大图片自动缩放:原始大小=" + originalSize + "MB,缩放后大小=" + scaledBitmap.getByteCount() / 1024 / 1024 + "MB"); + } - FileOutputStream fos = null; - try { - // 1. 生成唯一文件名(仅用于预览图,正式图由退出时拷贝生成) - String uniqueFileName = FileUtils.createUniqueFileName(new File("background.jpg")); - String compressFileName = "ScaledCompress_" + uniqueFileName; // 预览压缩图文件名 + // 1. 保存裁剪图到预览路径(调用工具类,统一路径管理) + File cropSaveFile = new File(mBgSourceUtils.getPreviewBackgroundFilePath()); + FileOutputStream fos = null; + BufferedOutputStream bos = null; + try { + // 清理旧文件(复用工具类public方法) + if (cropSaveFile.exists()) { + mBgSourceUtils.clearOldFileByExternal(cropSaveFile, "旧裁剪预览图"); + } + // 创建新文件(确保父目录存在,Java7 手动校验) + File parentDir = cropSaveFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + mBgSourceUtils.copyFile(new File(""), parentDir); // 复用目录创建逻辑 + } + // 父目录非空校验,避免创建文件失败 + if (parentDir == null) { + LogUtils.e(TAG, "【裁剪保存失败】目标文件父目录为空"); + ToastUtils.show("裁剪图片保存失败"); + return; + } + cropSaveFile.createNewFile(); - // 2. 定义预览图路径(统一目录,仅操作预览相关文件) - String backgroundDir = mBgSourceUtils.getBackgroundSourceDirPath(); - File targetPreviewOriginalFile = new File(backgroundDir, uniqueFileName); // 裁剪后预览正式图 - File targetPreviewCompressFile = new File(backgroundDir, compressFileName); // 裁剪后预览压缩图 + // 写入文件(Java7 手动管理流,无Lambda/try-with-resources) + fos = new FileOutputStream(cropSaveFile); + bos = new BufferedOutputStream(fos); + scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 85, bos); + bos.flush(); + // 强制同步到磁盘,避免文件损坏(兼容部分设备缓冲问题) + if (fos != null) { + try { + fos.getFD().sync(); + } catch (IOException e) { + LogUtils.w(TAG, "【裁剪保存】sync()调用失败,用flush()兜底:" + e.getMessage()); + bos.flush(); + } + } - // 确保目录存在(调用工具类,避免创建失败) - File parentDir = targetPreviewOriginalFile.getParentFile(); - if (!parentDir.exists()) { - mBgSourceUtils.copyFile(new File(""), parentDir); - } + LogUtils.d(TAG, "【裁剪保存】预览图保存成功:" + cropSaveFile.getAbsolutePath() + ",大小=" + cropSaveFile.length() + "bytes"); - // 3. 保存裁剪后的预览正式图(仅写入预览目录,不触碰正式图) - if (targetPreviewOriginalFile.exists()) { - mBgSourceUtils.clearOldFileByExternal(targetPreviewOriginalFile, "旧预览正式图"); - } - targetPreviewOriginalFile.createNewFile(); - mBgSourceUtils.setFilePermissions(targetPreviewOriginalFile); + // 2. 同步更新预览Bean(核心:仅操作预览Bean,不修改正式Bean) + mBgSourceUtils.saveFileToPreviewBean(cropSaveFile, "裁剪后图片"); + // 3. 生成压缩预览图(确保预览时加载缩略图,提升性能) + compressQualityToRecivedPicture(scaledBitmap); - fos = new FileOutputStream(targetPreviewOriginalFile); - Bitmap.CompressFormat format = targetPreviewOriginalFile.getName().endsWith(".png") ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG; - boolean previewOriginalSaveSuccess = scaledBitmap.compress(format, 80, fos); - fos.flush(); - fos.getFD().sync(); // 强制同步磁盘,确保文件写入完成 - fos.close(); // 关闭预览正式图流 + ToastUtils.show("裁剪图片保存成功"); + } catch (IOException e) { + LogUtils.e(TAG, "【裁剪保存失败】IO异常:" + e.getMessage(), e); + ToastUtils.show("裁剪图片保存失败"); + // 清理异常文件 + if (cropSaveFile.exists()) { + mBgSourceUtils.clearOldFileByExternal(cropSaveFile, "异常裁剪图"); + } + } finally { + // 关闭流(Java7 手动关闭,避免内存泄漏) + if (bos != null) { + try { + bos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【流关闭失败】BufferedOutputStream:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【流关闭失败】FileOutputStream:" + e.getMessage()); + } + } + // 回收Bitmap(避免OOM,Java7 手动判断) + if (scaledBitmap != bitmap && scaledBitmap != null && !scaledBitmap.isRecycled()) { + scaledBitmap.recycle(); + } + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } - // 4. 生成预览压缩图(预览Bean依赖此图,必须生成) - LogUtils.d(TAG, "【裁剪压缩】开始生成预览压缩图:" + targetPreviewCompressFile.getAbsolutePath()); - FileOutputStream compressFos = new FileOutputStream(targetPreviewCompressFile); - boolean previewCompressSaveSuccess = scaledBitmap.compress(format, 50, compressFos); // 压缩质量50%(按需调整) - compressFos.flush(); - compressFos.getFD().sync(); - compressFos.close(); - LogUtils.d(TAG, "【裁剪压缩】预览压缩图保存" + (previewCompressSaveSuccess ? "成功" : "失败") + ",大小:" + targetPreviewCompressFile.length() + "bytes"); + // 4. 清理裁剪临时文件(调用工具类,统一清理) + mBgSourceUtils.clearCropTempFiles(); + LogUtils.d(TAG, "【裁剪保存】流程结束,临时文件已清理"); + } - // 5. 关键逻辑:仅更新预览Bean(正式Bean不改动,留待退出时确认) - if (previewOriginalSaveSuccess && previewCompressSaveSuccess - && targetPreviewOriginalFile.length() > 100 - && targetPreviewCompressFile.length() > 100) { - // 获取当前预览Bean(窗口启动时已由正式Bean拷贝而来) - BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - // 仅更新预览Bean的路径信息(不操作正式Bean) - previewBean.setBackgroundFileName(uniqueFileName); // 预览正式图文件名 - previewBean.setBackgroundFilePath(targetPreviewOriginalFile.getAbsolutePath()); // 预览正式图路径 - previewBean.setBackgroundScaledCompressFileName(compressFileName); // 预览压缩图文件名 - previewBean.setBackgroundScaledCompressFilePath(targetPreviewCompressFile.getAbsolutePath()); // 预览压缩图路径 - previewBean.setIsUseBackgroundFile(true); // 标记预览图可用 - previewBean.setIsUseBackgroundScaledCompressFile(false); // 标记:未确认成为正式图(关键标记) - mBgSourceUtils.saveSettings(); // 仅持久化预览Bean(不保存正式Bean) + /** + * 辅助函数:缩放Bitmap(Java7 手动实现,不依赖Lambda/Stream) + */ + private Bitmap scaleBitmap(Bitmap bitmap, float scale) { + if (bitmap == null || bitmap.isRecycled() || scale <= 0) { + return bitmap; + } + 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.getWidth() + "x" + bitmap.getHeight() + ",缩放比例=" + scale + ",新宽高=" + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight()); + return scaledBitmap; + } - // 同步预览文件到预览Bean目录(工具类仅操作预览相关文件) - mBgSourceUtils.saveFileToPreviewBean(targetPreviewOriginalFile, targetPreviewOriginalFile.getAbsolutePath()); - mBgSourceUtils.saveFileToPreviewBean(targetPreviewCompressFile, targetPreviewCompressFile.getAbsolutePath()); - - ToastUtils.show("预览图片保存成功(未设为正式背景)"); - // 触发预览刷新(仅加载预览Bean中的路径) - doubleRefreshPreview(); - } else { - ToastUtils.show("预览图片保存失败"); - LogUtils.e(TAG, "【预览保存失败】正式图:" + previewOriginalSaveSuccess + ",压缩图:" + previewCompressSaveSuccess); - // 回滚预览Bean:标记为未使用文件 - BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - previewBean.setIsUseBackgroundFile(false); - previewBean.setIsUseBackgroundScaledCompressFile(false); - mBgSourceUtils.saveSettings(); - } - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪保存异常】" + e.getMessage(), e); - ToastUtils.show("预览图片保存失败"); - // 异常时回滚预览Bean - BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - previewBean.setIsUseBackgroundFile(false); - previewBean.setIsUseBackgroundScaledCompressFile(false); - mBgSourceUtils.saveSettings(); - } finally { - // 关闭流资源 - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "【裁剪保存异常】流关闭失败:" + e.getMessage()); - } - } - // 回收Bitmap - if (scaledBitmap != null && !scaledBitmap.isRecycled()) { - scaledBitmap.recycle(); - } - // 清理裁剪临时文件(仅清理缓存,不影响预览/正式文件) - mBgSourceUtils.clearCropTempFiles(); - } - } - - /** - * 辅助函数:缩放Bitmap(适配大图片) - */ - private Bitmap scaleBitmap(Bitmap bitmap, float scale) { - if (bitmap == null || scale <= 0 || scale >= 1.0f) { - return bitmap; - } - Matrix matrix = new Matrix(); - matrix.postScale(scale, scale); - return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - } - - /** - * 辅助函数:双重刷新预览(适配MIUI渲染延迟) - */ - private void doubleRefreshPreview() { - runOnUiThread(new Runnable() { + /** + * 双重刷新预览(适配MIUI渲染延迟,确保裁剪后立即显示,Java7 语法) + */ + private void doubleRefreshPreview() { + LogUtils.d(TAG, "【预览刷新】触发双重刷新(适配MIUI)"); + // 首次刷新(立即执行) + bvPreviewBackground.reloadPreviewBackground(); + // 延迟刷新(解决MIUI渲染延迟,Java7 Handler+匿名内部类) + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { - // 1. 同时校验正式图和压缩图(两者都存在才刷新) - String previewOriginalPath = mBgSourceUtils.getPreviewBackgroundFilePath(); - final String previewCompressPath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); - File originalFile = new File(previewOriginalPath); - File compressFile = new File(previewCompressPath); - - // 校验逻辑:正式图存在 + 压缩图存在 + 大小均>100字节(避免空文件) - boolean isPreviewReady = originalFile.exists() && originalFile.length() > 100 - && compressFile.exists() && compressFile.length() > 100; - - if (!isPreviewReady) { - LogUtils.w(TAG, "【预览刷新】预览图未就绪(正式图:" + originalFile.exists() + ",压缩图:" + compressFile.exists() + "),延迟500ms重试"); - // 延迟500ms重试(适配压缩图生成延迟) - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】延迟重试刷新(500ms),压缩图路径:" + previewCompressPath); - } - }, 500); - return; - } - - // 2. 第一次刷新(立即) bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第一次刷新(立即),正式图路径:" + previewOriginalPath + ",压缩图路径:" + previewCompressPath); + LogUtils.d(TAG, "【预览刷新】双重刷新完成"); + } + }, 300); // 300ms延迟,适配大多数机型 + } - // 3. 第二次刷新(延迟300ms,适配MIUI渲染延迟) - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + /** + * 检查图片类型是否为图片(辅助方法,Java7 语法) + */ + private boolean isImageType(String type) { + if (TextUtils.isEmpty(type)) { + return false; + } + return type.startsWith("image/"); + } + + /** + * 设置背景颜色(仅操作Bean,无文件逻辑)→ 修复:替换R.color.transparent为0x00000000(纯透明色值) + */ + private void setBackgroundColor() { + BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); + int pixelColor = bean.getPixelColor(); + if (pixelColor != 0) { + bvPreviewBackground.setBackgroundColor(pixelColor); + LogUtils.d(TAG, "【颜色设置】背景颜色已更新:" + pixelColor); + } else { + // 关键修复:用0x00000000(ARGB格式,A=0即透明)替代R.color.transparent,兼容所有项目 + bvPreviewBackground.setBackgroundColor(0x00000000); + LogUtils.d(TAG, "【颜色设置】背景颜色重置为透明"); + } + } + + /** + * 检查并申请存储权限(适配所有Android版本,移除READ_MEDIA_IMAGES,Java7 语法) + */ + private boolean checkAndRequestStoragePermission() { + LogUtils.d(TAG, "【权限检查】开始检查存储权限,Android版本:" + Build.VERSION.SDK_INT); + List needPermissions = new ArrayList(); + + // 统一用WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE,适配所有Android版本(避免READ_MEDIA_IMAGES找不到符号) + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + needPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + needPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE); + } + + // 申请必要权限(Java7 for循环,无增强for循环简化) + if (!needPermissions.isEmpty()) { + String[] permissionsArr = new String[needPermissions.size()]; + for (int i = 0; i < needPermissions.size(); i++) { + permissionsArr[i] = needPermissions.get(i); + } + ActivityCompat.requestPermissions(this, permissionsArr, STORAGE_PERMISSION_REQUEST); + LogUtils.d(TAG, "【权限申请】已触发权限申请:" + Arrays.toString(permissionsArr)); + return false; + } + + LogUtils.d(TAG, "【权限检查】存储权限已全部获取"); + return true; + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + LogUtils.d(TAG, "【权限回调】onRequestPermissionsResult 触发,requestCode:" + requestCode); + + if (requestCode == STORAGE_PERMISSION_REQUEST) { + boolean allGranted = true; + // 校验所有权限是否授予(Java7 for循环,无增强for循环) + for (int i = 0; i < grantResults.length; i++) { + if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + LogUtils.d(TAG, "【权限回调】权限未授予:" + permissions[i]); + break; + } + } + + if (allGranted) { + LogUtils.d(TAG, "【权限回调】所有存储权限已授予"); + ToastUtils.show("权限获取成功,请重新操作"); + } else { + LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝"); + // 检查是否勾选“不再询问”(适配Android 6.0+,Java7 for循环) + boolean shouldShowRationale = false; + for (int i = 0; i < permissions.length; i++) { + String permission = permissions[i]; + if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { + shouldShowRationale = true; + break; + } + } + + if (shouldShowRationale) { + // 未勾选“不再询问”:提示用户授予权限(Java7 匿名内部类) + new AlertDialog.Builder(this) + .setTitle("权限申请") + .setMessage("需要存储权限才能选择/拍照/裁剪图片,请授予权限") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override - public void run() { - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第二次刷新(延迟300ms),压缩图路径:" + previewCompressPath); + public void onClick(DialogInterface dialog, int which) { + checkAndRequestStoragePermission(); } - }, 300); - } - }); - } + }) + .setNegativeButton("取消", null) + .show(); + } else { + // 已勾选“不再询问”:引导用户去设置页开启权限(Java7 匿名内部类) + new AlertDialog.Builder(this) + .setTitle("权限被拒绝") + .setMessage("存储权限已被拒绝且勾选“不再询问”,请前往设置页开启权限") + .setPositiveButton("去设置", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + } + }) + .setNegativeButton("取消", null) + .show(); + } + } + } + } - /** - * 检查类型是否为图片(精简版,保留核心校验) - */ - private boolean isImageType(String type) { - if (TextUtils.isEmpty(type)) { - return false; - } - return type.startsWith("image/") || "image/jpeg".equals(type) || "image/png".equals(type); - } - - /** - * 检查并申请存储权限(精简版,仅保留核心权限校验) - */ - private boolean checkAndRequestStoragePermission() { - LogUtils.d(TAG, "【权限校验】checkAndRequestStoragePermission 触发,Android版本:" + Build.VERSION.SDK_INT); - - // Android14+:申请WRITE_EXTERNAL_STORAGE - if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { - boolean hasWritePerm = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - if (!hasWritePerm) { - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST); - return false; - } - } - - // Android11+:检查所有文件访问权限 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - boolean hasAllFilePerm = Environment.isExternalStorageManager(); - if (!hasAllFilePerm) { - startActivity(new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)); - ToastUtils.show("请开启「所有文件访问权限」"); - return false; - } - } - - // Android6.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; - if (!hasReadPerm || !hasWritePerm) { - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST); - return false; - } - } - - LogUtils.d(TAG, "【权限校验】存储权限校验通过"); - return true; - } - - /** - * 设置页面背景颜色(仅操作控件,无文件逻辑) - */ - void setBackgroundColor() { - LogUtils.d(TAG, "【背景设置】setBackgroundColor 触发"); - BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); - int pixelColor = bean.getPixelColor(); - LogUtils.d(TAG, "【背景设置】当前像素颜色:" + pixelColor); - - RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1); - if (mainLayout != null) { - mainLayout.setBackgroundColor(pixelColor); - } else { - LogUtils.e(TAG, "【背景设置】主布局为空,无法更新颜色"); - } - } - - @Override - protected void onResume() { - super.onResume(); - LogUtils.d(TAG, "【生命周期】onResume 触发"); - // 刷新背景颜色+预览 - setBackgroundColor(); - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【生命周期】onResume 完成"); - } - - /** - * 显示网络图片下载对话框(仅UI逻辑) - */ - public void onNetworkBackgroundDialog(View view) { - LogUtils.d(TAG, "【网络图片】onNetworkBackgroundDialog 触发"); - NetworkBackgroundDialog dialog = new NetworkBackgroundDialog(this, new NetworkBackgroundDialog.OnDialogClickListener() { - @Override - public void onConfirm(String szConfirmFilePath, String szConfirmFileUrl) { - LogUtils.d(TAG, "【网络图片】用户确认下载:" + szConfirmFilePath); - preViewFilePath = szConfirmFilePath; - preViewFileUrl = szConfirmFileUrl; - onRecivedPictureListener.onRecivedPicture(preViewFilePath, preViewFileUrl); - } - - @Override - public void onCancel() { - LogUtils.d(TAG, "【网络图片】用户取消下载"); - } - }); - dialog.setTitle("网络图片下载"); - dialog.setContent("是否下载该图片作为背景?"); - dialog.show(); - } - - /** - * 图片接收监听器(调用工具类同步预览,无文件逻辑) - */ - interface OnRecivedPictureListener { - void onRecivedPicture(String srcFilePath, String srcFileUrl); - } - - // 图片接收监听器实现(精简版,依赖工具类) - OnRecivedPictureListener onRecivedPictureListener = new OnRecivedPictureListener(){ - @Override - public void onRecivedPicture(String srcFilePath, String srcFileUrl) { - LogUtils.d(TAG, "【图片接收】onRecivedPicture 触发,文件路径:" + srcFilePath + ",文件Url:" + srcFileUrl); - // 校验文件路径有效性 - if (TextUtils.isEmpty(srcFilePath)) { - ToastUtils.show("网络图片路径为空"); - LogUtils.e(TAG, "【图片接收失败】图片路径为空,无法处理"); - return; - } - // 校验文件是否存在且有效(依赖工具类目录逻辑,无本地文件操作) - File srcFile = new File(srcFilePath); - LogUtils.d(TAG, "【图片接收校验】图片文件:路径=" + srcFile.getAbsolutePath() + ",是否存在=" + srcFile.exists() + ",文件大小=" + srcFile.length() + " bytes"); - if (!srcFile.exists() || srcFile.length() <= 0) { - ToastUtils.show("网络图片文件不存在或损坏"); - LogUtils.e(TAG, "【图片接收失败】图片文件无效,无法加载"); - return; - } - // 同步图片到预览Bean并刷新(调用工具类统一处理,无本地文件复制) - mBgSourceUtils.saveFileToPreviewBean(srcFile, srcFileUrl); - LogUtils.d(TAG, "【图片接收】图片已同步到预览Bean,刷新预览视图"); - // 刷新预览(确保图片正常显示) - bvPreviewBackground.reloadPreviewBackground(); - // 启动自由裁剪(调用工具类获取裁剪路径,无本地文件管理) - startCropImageActivity(true); - LogUtils.d(TAG, "【图片接收】已启动自由裁剪,裁剪路径由工具类管理"); - } - }; - - /** - * 重写finish方法,确保所有退出场景都触发确认提示(仅操作Bean,无文件逻辑) - */ - @Override - public void finish() { - LogUtils.d(TAG, "【生命周期】finish 触发,是否已提交配置:" + isCommitSettings); - // 未提交配置时,显示确认对话框(避免误退出丢失预览配置) - if (!isCommitSettings) { - LogUtils.d(TAG, "【退出确认】未提交配置,显示应用背景确认对话框"); - YesNoAlertDialog.show(this, "应用背景更改提示:", "是否应用预览图片?", new YesNoAlertDialog.OnDialogResultListener(){ - - @Override - public void onNo() { - // 用户选择“不应用”:保留原正式背景,仅更新启用状态(依赖工具类Bean) - LogUtils.d(TAG, "【退出确认】用户选择:不应用预览图片,保留原背景配置"); - isCommitSettings = true; // 标记为已提交,避免重复弹窗 - BackgroundBean currentBean = mBgSourceUtils.getCurrentBackgroundBean(); - // 根据预览路径是否为空,同步正式背景启用状态(避免残留无效配置) - currentBean.setIsUseBackgroundFile(!TextUtils.isEmpty(preViewFilePath)); - mBgSourceUtils.saveSettings(); // 调用工具类持久化原配置(仅更新启用状态) - LogUtils.d(TAG, "【退出配置】原背景配置保存完成,正式背景启用状态:" + currentBean.isUseBackgroundFile()); - // 退出前清理裁剪临时文件(调用工具类统一清理,无本地文件删除) - mBgSourceUtils.clearCropTempFiles(); - finish(); - } - - @Override - public void onYes() { - // 用户选择“应用”:将预览背景深拷贝到正式背景(调用工具类统一处理) - LogUtils.d(TAG, "【退出确认】用户选择:应用预览图片到正式背景"); - mBgSourceUtils.commitPreviewSourceToCurrent(); // 核心:工具类处理深拷贝+持久化 - isCommitSettings = true; // 标记为已提交 - LogUtils.d(TAG, "【退出配置】预览背景提交完成,正式背景已更新"); - ToastUtils.show("背景图片应用成功"); // 补充用户反馈,明确操作结果 - // 退出前清理裁剪临时文件(调用工具类统一清理) - mBgSourceUtils.clearCropTempFiles(); - finish(); - } - }); - } else { - // 已提交配置(或用户已选择弹窗选项),直接执行退出,避免循环 - LogUtils.d(TAG, "【生命周期】已提交配置,执行super.finish()正常退出"); - // 退出前清理裁剪临时文件(调用工具类统一清理) - mBgSourceUtils.clearCropTempFiles(); - super.finish(); - } - } - - /** - * 权限申请回调(仅处理权限结果,无文件逻辑) - */ - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - LogUtils.d(TAG, "【权限回调】onRequestPermissionsResult 触发,requestCode:" + requestCode); - // 处理存储权限申请结果 - if (requestCode == STORAGE_PERMISSION_REQUEST) { - boolean isGranted = false; - // 校验所有申请的权限是否都通过(读写权限需同时授予) - for (int i = 0; i < grantResults.length; i++) { - String perm = permissions[i]; - int result = grantResults[i]; - LogUtils.d(TAG, "【权限回调】权限:" + perm + ",申请结果:" + (result == PackageManager.PERMISSION_GRANTED ? "通过" : "拒绝")); - if (result == PackageManager.PERMISSION_GRANTED) { - isGranted = true; - } else { - isGranted = false; - break; - } - } - if (isGranted) { - ToastUtils.show("存储权限已获取,正在重试操作"); - LogUtils.d(TAG, "【权限回调】存储权限全部通过,自动重试上次操作"); - // 自动重试图片选择(无需用户再次点击按钮) - onSelectPictureClickListener.onClick(findViewById(R.id.activitybackgroundpictureAButton2)); - } else { - ToastUtils.show("需要存储权限才能选择/保存图片"); - LogUtils.e(TAG, "【权限回调】存储权限申请被拒绝,图片功能受限"); - } - } - } - - /** - * 兼容父类方法(若有重写需求,保留空实现或精简逻辑) - */ - @Override - public void onBackPressed() { - LogUtils.d(TAG, "【生命周期】onBackPressed 触发,执行finish"); - finish(); - } - - /** - * 避免内存泄漏:清空工具类引用(可选,根据父类生命周期规范补充) - */ - @Override - protected void onDestroy() { - super.onDestroy(); - LogUtils.d(TAG, "【生命周期】onDestroy 触发,清理资源"); - // 清空工具类引用(避免Activity销毁后持有引用导致内存泄漏) - if (mBgSourceUtils != null) { - mBgSourceUtils.clearCropTempFiles(); // 退出前最后清理一次临时文件 - } - mBgSourceUtils = null; - // 清空控件引用 - bvPreviewBackground = null; - mAToolbar = null; - LogUtils.d(TAG, "【生命周期】onDestroy 完成,资源清理完毕"); - } + @Override + protected void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "【生命周期】BackgroundSettingsActivity 销毁"); + // 清理临时资源(Java7 手动校验非空) + if (mfTakePhoto != null && mfTakePhoto.exists()) { + mBgSourceUtils.clearOldFileByExternal(mfTakePhoto, "拍照临时文件"); + } + mBgSourceUtils.clearCropTempFiles(); + // 清理压缩兜底路径的临时文件(避免残留) + File compressTempDir = new File(App.getTempDirPath(), "PreviewCompress"); + if (compressTempDir != null && compressTempDir.exists()) { + mBgSourceUtils.clearOldFileByExternal(compressTempDir, "压缩兜底临时目录"); + } + // 关键修复:删除不存在的 bvPreviewBackground.recycleBitmap() 调用 + // 原因:BackgroundView 源码中无此方法,且内部已通过 setDefaultTransparentBackground() 处理资源释放 + LogUtils.d(TAG, "Activity销毁:临时文件已清理,BackgroundView资源由自身生命周期管理"); + } } +