From ee4b0ca6d9d42e1f675171e0a78d854eb8c86726 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Mon, 1 Dec 2025 17:34:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=9D=83=E9=99=90=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=9B=BE=E7=89=87=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=89=AA=E8=A3=81=E9=83=A8=E5=88=86=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- powerbell/build.properties | 4 +- .../BackgroundSettingsActivity.java | 1173 ++++++++--------- .../utils/BackgroundSourceUtils.java | 194 ++- .../powerbell/utils/PermissionUtils.java | 214 +++ .../powerbell/views/BackgroundView.java | 154 ++- powerbell/src/main/res/values/strings.xml | 11 + 6 files changed, 1111 insertions(+), 639 deletions(-) create mode 100644 powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java diff --git a/powerbell/build.properties b/powerbell/build.properties index 3d0a27e9..aea26bbb 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Dec 01 07:58:43 GMT 2025 +#Mon Dec 01 09:33:34 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=52 +buildCount=59 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 efb470b4..802e6f8b 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 @@ -1,7 +1,5 @@ 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; @@ -31,6 +29,7 @@ import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; +import cc.winboll.studio.powerbell.utils.PermissionUtils; import cc.winboll.studio.powerbell.utils.UriUtil; import cc.winboll.studio.powerbell.views.BackgroundView; import java.io.BufferedOutputStream; @@ -43,25 +42,38 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import android.app.Activity; +import android.os.Environment; +import cc.winboll.studio.powerbell.utils.FileUtils; +/** + * 背景设置Activity(图片选择/拍照/裁剪/预览/保存核心页面) + * 核心特性: + * 1. 适配多包名场景(依赖工具类动态获取包名/Authority) + * 2. 兼容Android 6.0~14+(权限/存储/裁剪适配) + * 3. 采用工具类解耦(文件管理/权限管理分离,可维护性强) + * 4. 内存优化(Bitmap采样/缩放/回收,避免OOM) + * 5. 适配MIUI等特殊机型(裁剪权限/渲染延迟适配) + */ public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { + // 日志标签 public static final String TAG = "BackgroundSettingsActivity"; - // 工具类单例(唯一文件管理入口) + // 工具类单例(职责分离:文件管理/权限管理) private BackgroundSourceUtils mBgSourceUtils; + private PermissionUtils mPermissionUtils; - // 图片选择/裁剪请求码 + // 图片选择/裁剪/拍照请求码 public static final int REQUEST_SELECT_PICTURE = 0; public static final int REQUEST_TAKE_PHOTO = 1; public static final int REQUEST_CROP_IMAGE = 2; - private static final int STORAGE_PERMISSION_REQUEST = 100; - // 控件 + // 控件实例 private AToolbar mAToolbar; private BackgroundView bvPreviewBackground; // 拍照临时文件(仅拍照用,路径由工具类间接管理) private File mfTakePhoto; - // 配置标记 + // 配置标记(是否提交设置) boolean isCommitSettings = false; // 预览图片信息(用于退出确认) private String preViewFilePath = ""; @@ -81,27 +93,25 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_background_settings); + // 初始化核心控件 bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1); - // 初始化工具类(文件管理全依赖此类) + // 初始化工具类(单例模式,全局复用,解耦业务逻辑) mBgSourceUtils = BackgroundSourceUtils.getInstance(this); + mPermissionUtils = PermissionUtils.getInstance(); - // 初始化拍照临时文件(路径复用App临时目录,权限由工具类保障) + // 初始化拍照临时文件(复用App临时目录,权限由PermissionUtils保障) File tempDir = new File(App.getTempDirPath()); if (!tempDir.exists()) { tempDir.mkdirs(); } mfTakePhoto = new File(tempDir, "TakePhoto.jpg"); - // 初始化工具栏 + // 初始化UI及数据 initToolbar(); - // 初始化按钮点击事件(仅UI交互,无文件逻辑) initClickListeners(); - // 初始化预览 - // 关键:窗口启动时,将正式Bean的内容完整拷贝到预览Bean(覆盖旧预览Bean) - initPreviewBeanFromFormal(); - // 处理分享图片意图 - handleShareIntent(); + initPreviewBeanFromFormal(); // 正式Bean → 预览Bean(确保预览数据准确) + handleShareIntent(); // 处理分享图片意图 LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(Java7 语法版)"); } @@ -112,13 +122,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg private void initPreviewBeanFromFormal() { LogUtils.d(TAG, "【Bean初始化】正式Bean → 预览Bean(拷贝初始化)"); mBgSourceUtils.setCurrentSourceToPreview(); - // 加载预览Bean中的图片(窗口启动时显示正式图的预览) - bvPreviewBackground.reloadPreviewBackground(); + bvPreviewBackground.reloadPreviewBackground(); // 加载预览图 LogUtils.d(TAG, "【Bean初始化】预览Bean初始化完成,当前预览路径:" + mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath()); } /** - * 初始化工具栏(仅UI逻辑) + * 初始化工具栏(仅UI逻辑,无业务逻辑) */ private void initToolbar() { mAToolbar = (AToolbar) findViewById(R.id.toolbar); @@ -135,7 +144,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 初始化所有按钮点击事件(仅UI交互,逻辑调用工具类) + * 初始化所有按钮点击事件(仅UI交互,业务逻辑调用工具类) */ private void initClickListeners() { findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener); @@ -171,40 +180,49 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 更新背景预览(强制使用预览Bean,删除正式Bean分支,Java7语法) + * 更新背景预览(强制使用预览Bean,全程不操作正式Bean) */ public void updateBackgroundView(File sourceFile, String sourceFileInfo) { LogUtils.d(TAG, "【预览更新】updateBackgroundView 触发,sourceFile是否为空:" + (sourceFile == null)); if (sourceFile != null) { - mBgSourceUtils.saveFileToPreviewBean(sourceFile, sourceFileInfo); // 有文件时同步到预览Bean + mBgSourceUtils.saveFileToPreviewBean(sourceFile, sourceFileInfo); // 同步到预览Bean } - // 强制加载预览Bean(无论sourceFile是否为空,全程不使用正式Bean) - bvPreviewBackground.reloadPreviewBackground(); + bvPreviewBackground.reloadPreviewBackground(); // 强制加载预览Bean LogUtils.d(TAG, "【预览更新】预览背景更新完成(强制使用previewBackgroundBean)"); } - // 点击事件:取消背景(仅操作Bean,无文件逻辑) + // ======================================== 按钮点击事件(仅UI交互) ======================================== + /** + * 点击事件:取消背景(仅操作Bean,无文件逻辑) + */ private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【按钮点击】触发取消背景功能"); - BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); - bean.setIsUseBackgroundFile(false); - bean.resetBackgroundConfig(); - mBgSourceUtils.saveSettings(); - bvPreviewBackground.reloadPreviewBackground(); - ToastUtils.show("背景已取消"); - } - }; + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发取消背景功能"); + // 1. 修改正式Bean + BackgroundBean formalBean = mBgSourceUtils.getCurrentBackgroundBean(); + formalBean.setIsUseBackgroundFile(false); + formalBean.resetBackgroundConfig(); + mBgSourceUtils.saveSettings(); + // 2. 同步预览Bean(关键优化:确保控件刷新后显示“取消背景”状态) + mBgSourceUtils.setCurrentSourceToPreview(); + // 3. 控件刷新(仍依赖预览Bean) + bvPreviewBackground.reloadPreviewBackground(); + ToastUtils.show("背景已取消"); + } + }; - // 点击事件:选择图片(仅权限+意图,文件处理调用工具类) + /** + * 点击事件:选择图片(权限校验调用PermissionUtils,文件处理调用BackgroundSourceUtils) + */ private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】触发选择图片功能"); - if (checkAndRequestStoragePermission()) { + // 调用权限工具类校验存储权限 + if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) { LogUtils.d(TAG, "【选图权限】存储权限已获取,开始查找图片选择意图"); - // 多意图兜底 + // 多意图兜底(适配不同相册应用) Intent[] intents = new Intent[3]; // 意图1:ACTION_GET_CONTENT(优先) Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); @@ -273,12 +291,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }; - // 点击事件:固定比例裁剪(调用工具类获取裁剪路径,无文件逻辑) + /** + * 点击事件:固定比例裁剪(仅触发裁剪,文件路径由工具类管理) + */ private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能"); - // 从工具类获取正式背景文件,校验有效性 File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath()); if (targetFile.exists()) { // 适配MIUI:弹出裁剪提示(建议选择系统相机) @@ -295,8 +314,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg .setNegativeButton("取消", null) .show(); } else { - // 非MIUI机型直接启动裁剪 - startCropImageActivity(false); + startCropImageActivity(false); // 非MIUI机型直接启动裁剪 } LogUtils.d(TAG, "【裁剪启动】固定比例裁剪已启动"); } else { @@ -306,7 +324,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }; - // 点击事件:自由裁剪(调用工具类获取裁剪路径,无文件逻辑) + /** + * 点击事件:自由裁剪(仅触发裁剪,文件路径由工具类管理) + */ private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { @@ -327,8 +347,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg .setNegativeButton("取消", null) .show(); } else { - // 非MIUI机型直接启动裁剪 - startCropImageActivity(true); + startCropImageActivity(true); // 非MIUI机型直接启动裁剪 } LogUtils.d(TAG, "【裁剪启动】自由裁剪已启动"); } else { @@ -338,7 +357,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }; - // 点击事件:拍照(仅权限+相机意图,文件处理调用工具类) + /** + * 点击事件:拍照(权限校验调用PermissionUtils,文件处理调用BackgroundSourceUtils) + */ private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { @Override public void onClick(View v) { @@ -362,8 +383,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - // 检查权限后启动相机 - if (checkAndRequestStoragePermission()) { + // 调用权限工具类校验存储权限 + if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) { LogUtils.d(TAG, "【拍照权限】存储权限已获取,开始生成拍照Uri"); Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { @@ -384,7 +405,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }; - // 点击事件:图片接收(暂未实现) + /** + * 点击事件:图片接收(暂未实现) + */ private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { @@ -393,12 +416,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }; - // 点击事件:像素拾取(调用工具类获取图片路径,无文件逻辑) + /** + * 点击事件:像素拾取(路径由工具类管理,仅触发跳转) + */ private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() { @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】触发像素拾取功能"); - // 从工具类获取正式背景路径 String targetImagePath = mBgSourceUtils.getCurrentBackgroundFilePath(); File targetFile = new File(targetImagePath); if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { @@ -414,7 +438,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }; - // 点击事件:清空像素颜色(仅操作Bean,无文件逻辑) + /** + * 点击事件:清空像素颜色(仅操作Bean,无文件逻辑) + */ private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() { @Override public void onClick(View v) { @@ -430,108 +456,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg }; /** - * 压缩图片并保存(核心修复:路径非空校验+兜底路径,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; - 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 fRecivedPicture = new File(scaledCompressFilePath); - LogUtils.d(TAG, "【压缩配置】目标压缩路径:" + scaledCompressFilePath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); - - // 核心修复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()); - } - - // 创建目标文件(调用工具类清理旧文件) - if (fRecivedPicture.exists()) { - mBgSourceUtils.clearOldFileByExternal(fRecivedPicture, "旧压缩文件"); - } - fRecivedPicture.createNewFile(); - - // 压缩并保存(Java7 手动管理流) - fos = new FileOutputStream(fRecivedPicture); - outStream = new BufferedOutputStream(fos); - boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); - outStream.flush(); - - // 强制同步数据到磁盘(兼容部分设备) - if (fos != null) { - try { - fos.getFD().sync(); - LogUtils.d(TAG, "【压缩保存】已强制同步数据到磁盘"); - } catch (IOException e) { - LogUtils.w(TAG, "【压缩保存】sync()调用失败,用flush()兜底:" + e.getMessage()); - outStream.flush(); - } - } - - 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 语法) + * 启动图片裁剪活动(核心:调用工具类获取裁剪路径,适配MIUI) * @param isCropFree 是否自由裁剪 */ public void startCropImageActivity(boolean isCropFree) { @@ -540,7 +465,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg previewBean.setIsUseBackgroundScaledCompressFile(true); mBgSourceUtils.saveSettings(); - // 1. 预览图片有效性校验(保留强化校验逻辑) + // 1. 预览图片有效性校验 String previewFilePath = mBgSourceUtils.getPreviewBackgroundFilePath(); if (TextUtils.isEmpty(previewFilePath)) { ToastUtils.show("预览图片路径为空"); @@ -569,7 +494,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - // 3. 调用工具类创建裁剪路径(原有逻辑不变) + // 3. 调用工具类创建裁剪路径 File cropTempFile = mBgSourceUtils.createCropFileProviderPath(); if (cropTempFile == null) { ToastUtils.show("裁剪路径创建失败,请重试"); @@ -578,12 +503,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 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; @@ -595,8 +519,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg intent.putExtra("aspectY", 1); } - // 输出尺寸设置(原有逻辑不变) - int maxOutputSize = 2048; // 移除Android13+判断,统一用2048(适配所有版本) + // 输出尺寸设置(统一用2048,适配所有版本) + int maxOutputSize = 2048; int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize); int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize); intent.putExtra("outputX", outputX); @@ -609,14 +533,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg intent.putExtra("quality", 80); intent.putExtra("return-data", false); // 禁用返回Bitmap,避免OOM - // 权限Flags(添加持久化权限,适配MIUI) + // 权限Flags(添加持久化权限) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); } - // 5. 适配系统裁剪工具(MIUI专属处理,Java7 for循环) + // 5. 适配系统裁剪工具(MIUI专属处理) try { List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); if (!resolveInfos.isEmpty()) { @@ -633,7 +557,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【裁剪适配】已授予MIUI裁剪工具输出Uri写入权限"); } - // 显式设置Component(移除setDataAndType(null, null),避免参数冲突) + // 显式设置Component Intent cropIntent = new Intent(intent); cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); @@ -642,7 +566,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg 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, "选择裁剪工具"); @@ -661,76 +585,186 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - /** - * 计算最大公约数(简化裁剪比例) - */ - private int calculateGCD(int a, int b) { - if (b == 0) { - return a; - } - return calculateGCD(b, a % b); - } + /** + * 保存裁剪后的图片(修复:路径同步时序+压缩图路径强绑定) + * 作用:裁剪后保存原图→生成压缩图→同步双路径到预览Bean→清理临时文件 + */ + private void saveCropBitmap(Bitmap bitmap) { + LogUtils.d(TAG, "【保存启动】开始保存裁剪图片(仅更新预览Bean,不影响正式Bean)"); + if (bitmap == null || bitmap.isRecycled()) { + ToastUtils.show("裁剪图片为空"); + mBgSourceUtils.clearCropTempFiles(); + return; + } - /** - * 分享图片(调用工具类获取路径,精简文件逻辑) - */ - void sharePicture() { - LogUtils.d(TAG, "【分享启动】sharePicture 触发"); - // 从工具类获取正式背景路径 - String currentBgPath = mBgSourceUtils.getCurrentBackgroundFilePath(); - File shareFile = new File(currentBgPath); - if (!shareFile.exists() || shareFile.length() <= 0) { - ToastUtils.show("分享的背景图片不存在"); - LogUtils.e(TAG, "【分享失败】分享图片无效"); - return; - } + // 内存优化:大图片自动缩放(保留原有逻辑,避免OOM) + Bitmap scaledBitmap = bitmap; + int originalSize = bitmap.getByteCount() / 1024 / 1024; + if (originalSize > 5) { + float scale = 1.0f; + while (scaledBitmap.getByteCount() / 1024 / 1024 > 2) { + scale -= 0.2f; + if (scale < 0.2f) break; + scaledBitmap = scaleBitmap(scaledBitmap, scale); // 复用原有缩放方法 + } + LogUtils.d(TAG, "【内存优化】大图片自动缩放:原始大小=" + originalSize + "MB,缩放后大小=" + scaledBitmap.getByteCount() / 1024 / 1024 + "MB"); + } - try { - // 生成分享Uri(调用工具类的Authority) - Uri shareUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), shareFile); - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri); - shareIntent.setType("image/jpeg"); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + File cropSaveFile = new File(mBgSourceUtils.getPreviewBackgroundFilePath()); + FileOutputStream fos = null; + BufferedOutputStream bos = null; + try { + // 1. 清理旧的裁剪预览图(避免文件残留) + if (cropSaveFile.exists()) { + mBgSourceUtils.clearOldFileByExternal(cropSaveFile, "旧裁剪预览图"); + } + // 确保父目录存在(兼容Android 10+分区存储,多包名环境下目录适配) + 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(); - Intent chooser = Intent.createChooser(shareIntent, "选择分享方式"); - if (chooser.resolveActivity(getPackageManager()) != null) { - startActivity(chooser); - LogUtils.d(TAG, "【分享启动】已启动分享选择器"); - } else { - ToastUtils.show("无可用分享应用"); - } - } catch (Exception e) { - LogUtils.e(TAG, "【分享异常】分享失败:" + e.getMessage(), e); - ToastUtils.show("分享失败,请重试"); - } - } + // 2. 写入裁剪原图到指定路径(覆盖旧文件) + fos = new FileOutputStream(cropSaveFile); + bos = new BufferedOutputStream(fos); + scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 85, bos); + bos.flush(); + // 强制同步到磁盘(避免Android 10+异步写入导致文件读取不到) + if (fos != null) { + try { + fos.getFD().sync(); + } catch (IOException e) { + LogUtils.w(TAG, "【裁剪保存】sync()调用失败,用flush()兜底:" + e.getMessage()); + bos.flush(); + } + } + LogUtils.d(TAG, "【裁剪保存】预览原图保存成功:" + cropSaveFile.getAbsolutePath() + ",大小=" + cropSaveFile.length() + "bytes"); + // 3. 生成新压缩图(关键:先获取/生成压缩路径,再压缩) + String newCompressPath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); + // 兜底:若压缩路径为空,生成新的唯一路径(避免空指针,兼容多包名路径隔离) + if (TextUtils.isEmpty(newCompressPath)) { + String sourceDir = mBgSourceUtils.getBackgroundSourceDirPath(); + newCompressPath = new File(sourceDir, "ScaledCompress_" + System.currentTimeMillis() + ".jpg").getAbsolutePath(); + } + // 调用重载方法压缩,传入指定压缩路径 + mBgSourceUtils.compressQualityToRecivedPicture(scaledBitmap, newCompressPath); + + // 4. 同步更新预览Bean(核心修复:同时绑定原图路径+压缩图路径) + mBgSourceUtils.saveFileToPreviewBean(cropSaveFile, "裁剪后图片"); // 原有方法:更新原图路径 + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + if (previewBean != null) { + previewBean.setBackgroundScaledCompressFilePath(newCompressPath); // 新增:绑定压缩图路径 + mBgSourceUtils.saveSettings(); // 持久化Bean配置,避免路径丢失(多包名环境下配置隔离) + LogUtils.d(TAG, "【路径同步】预览Bean双路径同步完成:"); + LogUtils.d(TAG, "→ 原图路径:" + previewBean.getBackgroundFilePath()); + LogUtils.d(TAG, "→ 压缩图路径:" + previewBean.getBackgroundScaledCompressFilePath()); + } else { + LogUtils.e(TAG, "【路径同步失败】预览Bean为空"); + } + + ToastUtils.show("裁剪图片保存成功"); + } catch (IOException e) { + LogUtils.e(TAG, "【裁剪保存失败】IO异常:" + e.getMessage(), e); + ToastUtils.show("裁剪图片保存失败"); + // 异常时清理无效文件 + if (cropSaveFile.exists()) { + mBgSourceUtils.clearOldFileByExternal(cropSaveFile, "异常裁剪图"); + } + } finally { + // 资源回收(避免内存泄漏,兼容低版本Android) + 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(避免重复回收) + if (scaledBitmap != bitmap && scaledBitmap != null && !scaledBitmap.isRecycled()) { + scaledBitmap.recycle(); + } + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + + // 5. 清理裁剪临时文件(保留原有逻辑,避免临时文件堆积) + mBgSourceUtils.clearCropTempFiles(); + LogUtils.d(TAG, "【裁剪保存】流程结束,临时文件已清理"); + } + + // ======================================== Activity回调处理(选图/拍照/裁剪) ======================================== @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - LogUtils.d(TAG, "【回调触发】onActivityResult 触发,requestCode:" + requestCode + ",resultCode:" + resultCode); + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + LogUtils.d(TAG, "【回调触发】onActivityResult 触发,requestCode:" + requestCode + ",resultCode:" + resultCode); - try { - // 事件分发(仅调用对应处理方法,无文件逻辑) - if (requestCode == REQUEST_SELECT_PICTURE) { - handleSelectPictureResult(resultCode, data); - } else if (requestCode == REQUEST_TAKE_PHOTO) { - handleTakePhotoResult(resultCode, data); - } else if (requestCode == REQUEST_CROP_IMAGE) { - handleCropImageResult(requestCode, resultCode, data); - } else if (resultCode != RESULT_OK) { - handleOperationCancelOrFail(); - } - } catch (Exception e) { - LogUtils.e(TAG, "【回调异常】onActivityResult 全局异常:" + e.getMessage(), e); - ToastUtils.show("操作失败,请重试"); - mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理临时文件 - } - } + try { + // 分支1:处理 Android13+ 存储管理权限回调(仅对应权限请求码) + if (requestCode == PermissionUtils.REQUEST_MANAGE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + handleStoragePermissionCallback(); + return; // 避免进入后续图片回调逻辑 + } + + // 分支2:处理图片操作回调(选图/拍照/裁剪)- 核心修复 + if (resultCode != RESULT_OK) { + // 先处理“操作取消/失败”(所有图片操作的通用逻辑) + handleOperationCancelOrFail(); + return; + } + + // 分发对应图片操作的回调(精准匹配 requestCode) + switch (requestCode) { + case REQUEST_SELECT_PICTURE: + handleSelectPictureResult(resultCode, data); // 选图结果处理(此次缺失的逻辑) + break; + case REQUEST_TAKE_PHOTO: + handleTakePhotoResult(resultCode, data); // 拍照结果处理 + break; + case REQUEST_CROP_IMAGE: + handleCropImageResult(requestCode, resultCode, data); // 裁剪结果处理 + break; + default: + LogUtils.d(TAG, "【回调忽略】未知 requestCode:" + requestCode + ",无需处理"); + break; + } + } catch (Exception e) { + LogUtils.e(TAG, "【回调异常】onActivityResult 全局异常:" + e.getMessage(), e); + ToastUtils.show("操作失败,请重试"); + mBgSourceUtils.clearCropTempFiles(); // 异常时清理临时文件,避免残留 + } + } + + /** + * 单独封装:Android13+ 存储权限回调处理(解耦,提升可维护性) + */ + private void handleStoragePermissionCallback() { + if (Environment.isExternalStorageManager()) { + LogUtils.d(TAG, "【权限回调】Android13+ 存储管理权限已授予"); + ToastUtils.show("存储权限已获取,可正常使用图片功能"); + } else { + LogUtils.d(TAG, "【权限回调】Android13+ 存储管理权限已拒绝"); + ToastUtils.show("存储权限不足,部分图片功能可能无法使用"); + } + } /** - * 处理拍照回调(调用工具类同步预览,精简文件操作) + * 处理拍照回调(调用工具类同步预览) */ private void handleTakePhotoResult(int resultCode, Intent data) { if (resultCode != RESULT_OK || data == null) { @@ -748,14 +782,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 获取Bitmap并压缩保存 Bitmap photoBitmap = getTakePhotoBitmap(data); if (photoBitmap != null && !photoBitmap.isRecycled()) { - compressQualityToRecivedPicture(photoBitmap); + mBgSourceUtils.compressQualityToRecivedPicture(photoBitmap); } else { ToastUtils.show("拍照图片为空"); mBgSourceUtils.clearCropTempFiles(); return; } - // 同步预览并启动裁剪(调用工具类保存预览) + // 同步预览并启动裁剪 mBgSourceUtils.saveFileToPreviewBean(mfTakePhoto, mfTakePhoto.getAbsolutePath()); bvPreviewBackground.reloadPreviewBackground(); startCropImageActivity(false); @@ -763,7 +797,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 处理裁剪回调(调用工具类获取裁剪文件,精简文件操作) + * 处理裁剪回调(调用工具类获取裁剪文件) */ private void handleCropImageResult(int requestCode, int resultCode, Intent data) { // 从工具类获取裁剪临时文件(唯一入口) @@ -771,13 +805,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg boolean isFileExist = cropTempFile != null && cropTempFile.exists(); boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; long fileSize = isFileExist ? cropTempFile.length() : 0; - // 适配MIUI:仅resultCode=RESULT_OK且文件有效时视为成功(resultCode=0视为取消) + // 适配MIUI:仅resultCode=RESULT_OK且文件有效时视为成功 boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100; // 打印校验日志 LogUtils.d(TAG, "【裁剪回调】校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + ",大小=" + fileSize + "bytes,是否成功=" + isCropSuccess); - // 处理MIUI裁剪取消(resultCode=0视为取消,而非失败) + // 处理MIUI裁剪取消(resultCode=0视为取消) if (resultCode == 0 && !isCropSuccess) { LogUtils.d(TAG, "【裁剪回调】MIUI 裁剪工具已取消"); ToastUtils.show("裁剪已取消"); @@ -793,7 +827,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - // 裁剪成功:解析Bitmap并保存(原有逻辑不变) + // 裁剪成功:解析Bitmap并保存 if (isCropSuccess) { Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); if (cropBitmap != null && !cropBitmap.isRecycled()) { @@ -806,20 +840,90 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mBgSourceUtils.clearCropTempFiles(); } } else { - // 其他失败场景(统一处理) + // 其他失败场景 handleOperationCancelOrFail(); } } /** - * 处理所有操作取消/失败(统一清理+提示,无文件逻辑) + * 处理选图回调(新增:目标文件有效性校验) + */ + /** + * 处理选图回调(新增:目标文件有效性校验 + 压缩路径时序修正) + */ + 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()); + + // 授予持久化权限(Android4.4+) + grantPersistableUriPermission(selectedImage); + + // 解析Uri为文件(核心:使用修复后的流复制方法) + File selectedFile = parseUriToFile(selectedImage); + if (selectedFile == null || !selectedFile.exists() || selectedFile.length() <= 0) { + ToastUtils.show("选择的图片文件无效"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 【核心修复1:先同步预览Bean(更新路径),再生成压缩图(确保路径与文件匹配)】 + mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString()); + // 【核心修复2:获取预览Bean中最新的压缩路径,作为压缩目标路径】 + String targetCompressPath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); + if (TextUtils.isEmpty(targetCompressPath)) { + ToastUtils.show("选图后压缩路径生成失败"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 关键修复:选图后生成压缩图(指定目标路径,避免路径不匹配) + LogUtils.d(TAG, "【选图压缩】开始生成预览压缩图,目标路径:" + targetCompressPath); + Bitmap selectBitmap = BitmapFactory.decodeFile(selectedFile.getAbsolutePath()); + if (selectBitmap != null && !selectBitmap.isRecycled()) { + mBgSourceUtils.compressQualityToRecivedPicture(selectBitmap, targetCompressPath); // 调用重载函数 + selectBitmap.recycle(); // 回收Bitmap + } else { + ToastUtils.show("选图后压缩图生成失败"); + LogUtils.e(TAG, "【选图压缩失败】无法解析选图文件为Bitmap"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 【核心修复3:无需重复调用saveFileToPreviewBean(已提前同步)】 + bvPreviewBackground.reloadPreviewBackground(); + startCropImageActivity(false); + LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪(正式图+压缩图均已生成)"); + } + + /** + * 处理所有操作取消/失败(统一清理+提示) */ private void handleOperationCancelOrFail() { LogUtils.d(TAG, "【操作回调】操作取消或失败"); ToastUtils.show("操作已取消"); - mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理裁剪临时文件 + mBgSourceUtils.clearCropTempFiles(); } + // ======================================== 权限回调(转发给工具类处理) ======================================== + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + LogUtils.d(TAG, "【权限回调】Activity回调触发,转发给PermissionUtils处理"); + // 调用权限工具类统一处理回调(自动过滤非存储权限) + mPermissionUtils.handleStoragePermissionResult(this, requestCode, permissions, grantResults); + } + + // ======================================== 辅助工具方法(私有,封装细节) ======================================== /** * 辅助函数:为选图Uri添加持久化读取权限(Android4.4+) */ @@ -834,31 +938,29 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 辅助函数:解析选图Uri为File(核心修复:Android14+ 共享存储私有路径适配) - * 放弃直接路径读取,改用ContentResolver流复制,避免Permission denied + * 辅助函数:解析选图Uri为File(适配Android14+ 共享存储私有路径) */ private File parseUriToFile(Uri uri) { File targetFile = null; - // 1. 尝试解析路径(兼容旧版本/普通路径) + // 1. 尝试解析路径(兼容旧版本) String filePath = UriUtil.getFilePathFromUri(this, uri); LogUtils.d(TAG, "【选图解析】Uri解析路径:" + filePath); - // 2. 路径有效且可读取(兼容Android 10- 或 非隐藏路径) + // 2. 路径有效且可读取 if (!TextUtils.isEmpty(filePath)) { File tempFile = new File(filePath); - // 双重校验:文件存在 + 实际可读取(避免canRead()假阳性) if (isFileActuallyReadable(tempFile)) { targetFile = tempFile; } else { - // 路径存在但无权限 → 流复制兜底(核心修复) + // 路径存在但无权限 → 流复制兜底 targetFile = createTempFileByStreamCopy(uri); } } else { - // 3. 路径解析失败(ContentProvider Uri)→ 流复制兜底 + // 3. 路径解析失败 → 流复制兜底 targetFile = createTempFileByStreamCopy(uri); } - // 4. 校验目标文件有效性(避免后续逻辑崩溃) + // 4. 校验目标文件有效性 if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { LogUtils.e(TAG, "【选图解析】生成的目标文件无效:" + (targetFile != null ? targetFile.getAbsolutePath() : "null")); ToastUtils.show("图片读取失败,请重新选择"); @@ -869,21 +971,20 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 辅助函数:通过ContentResolver流复制生成临时文件(核心修复:绕开共享存储权限限制) - * 直接读取Uri流,不依赖文件路径,适配所有相册Uri(包括私有隐藏路径),Java7语法 + * 辅助函数:通过ContentResolver流复制生成临时文件(绕开共享存储权限限制) */ private File createTempFileByStreamCopy(Uri uri) { - // 1. 初始化临时目录(复用工具类目录,统一路径管理) + // 1. 初始化临时目录 File tempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp"); if (!tempDir.exists()) { - mBgSourceUtils.copyFile(new File(""), tempDir); // 复用工具类目录创建逻辑 + mBgSourceUtils.copyFile(new File(""), tempDir); } - // 2. 生成唯一临时文件名(避免重复) + // 2. 生成唯一临时文件名 String uniqueFileName = "selected_temp_" + System.currentTimeMillis() + ".jpg"; File tempFile = new File(tempDir, uniqueFileName); - // 3. 流复制(核心:用ContentResolver打开Uri流,绕开路径权限限制,Java7手动关闭流) + // 3. 流复制(Java7手动关闭流) InputStream is = null; FileOutputStream fos = null; try { @@ -892,7 +993,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mBgSourceUtils.clearOldFileByExternal(tempFile, "旧选图临时文件"); } - // 打开Uri输入流(关键:Android14+ 仅允许通过ContentResolver读取共享存储私有文件) + // 打开Uri输入流 is = getContentResolver().openInputStream(uri); if (is == null) { LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败:" + uri.toString()); @@ -901,14 +1002,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 打开目标文件输出流 fos = new FileOutputStream(tempFile); - byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,提升复制效率 + 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(); // 确保数据写入磁盘(避免缓冲导致文件损坏) + fos.getFD().sync(); // 确保数据写入磁盘 LogUtils.d(TAG, "【选图解析】流复制成功:" + tempFile.getAbsolutePath() + ",大小:" + tempFile.length() + "bytes"); } catch (Exception e) { @@ -916,7 +1016,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg tempFile = null; ToastUtils.show("图片读取失败,请重新选择"); } finally { - // 关闭流资源(Java7 手动关闭,避免内存泄漏) + // 关闭流资源 if (is != null) { try { is.close(); @@ -936,13 +1036,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 辅助函数:校验文件是否实际可读取(解决Android14+ canRead()假阳性问题) + * 辅助函数:校验文件是否实际可读取(解决Android14读取(解决Android14+ canRead()假阳性问题) */ private boolean isFileActuallyReadable(File file) { if (file == null || !file.exists() || !file.isFile()) { return false; } - // 实际尝试读取文件(避免路径存在但无权限) + // 实际尝试读取文件 FileInputStream fis = null; try { fis = new FileInputStream(file); @@ -963,55 +1063,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 处理选图回调(新增:目标文件有效性校验,避免后续逻辑崩溃) - */ - 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()); - - // 授予持久化权限(Android4.4+) - grantPersistableUriPermission(selectedImage); - - // 解析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; - } - - // 同步到预览并启动裁剪(此时正式图和压缩图均已存在) - mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString()); - bvPreviewBackground.reloadPreviewBackground(); - startCropImageActivity(false); - LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪(正式图+压缩图均已生成)"); - } - - /** - * 辅助函数:从拍照数据中获取Bitmap(精简版,无文件操作) + * 辅助函数:从拍照数据中获取Bitmap */ private Bitmap getTakePhotoBitmap(Intent data) { Bundle extras = data.getExtras(); @@ -1027,333 +1079,222 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * 辅助函数:解析裁剪临时文件为Bitmap(适配大图片OOM) */ private Bitmap parseCropTempFileToBitmap(File cropTempFile) { - if (cropTempFile == null || !cropTempFile.exists() || cropTempFile.length() <= 100) { - return null; - } + 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(避免大图片OOM) + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); + LogUtils.d(TAG, "【裁剪解析】图片原始尺寸:宽=" + options.outWidth + ",高=" + options.outHeight + ",格式=" + options.outMimeType); - // 自动适配格式+计算采样率 - options.inPreferredConfig = getBitmapConfigByMimeType(options.outMimeType); - options.inSampleSize = calculateBitmapSampleRate(options, 2048); + // 第二步:计算采样率(适配屏幕尺寸,最大缩放至2048px以内) + int maxOutputSize = 2048; + int sampleSize = 1; + while (options.outWidth / sampleSize > maxOutputSize || options.outHeight / sampleSize > maxOutputSize) { + sampleSize *= 2; + } + LogUtils.d(TAG, "【裁剪解析】计算采样率:" + sampleSize + "(目标最大尺寸:" + maxOutputSize + "px)"); - // 第二步:正式加载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; + options.inSampleSize = sampleSize; + options.inPreferredConfig = Bitmap.Config.RGB_565; // 降低色彩精度,减少内存占用(比ARGB_8888节省50%内存) + options.inPurgeable = true; + options.inInputShareable = true; - /** - * 辅助函数:根据图片格式适配Bitmap配置 - */ - private Bitmap.Config getBitmapConfigByMimeType(String mimeType) { - if (mimeType != null && mimeType.contains("png")) { - return Bitmap.Config.ARGB_8888; - } else { - return Bitmap.Config.RGB_565; - } - } + cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); + LogUtils.d(TAG, "【裁剪解析】Bitmap加载结果:" + (cropBitmap != null ? "成功" : "失败") + + ",加载后尺寸:" + (cropBitmap != null ? cropBitmap.getWidth() + "x" + cropBitmap.getHeight() : "null")); - /** - * 辅助函数:计算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; - } + // 第四步:纠正图片旋转角度(解决部分机型拍照/裁剪后图片颠倒问题) + if (cropBitmap != null) { + int rotateAngle = mBgSourceUtils.getImageRotateAngle(cropTempFile.getAbsolutePath()); + if (rotateAngle != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(rotateAngle); + Bitmap rotatedBitmap = Bitmap.createBitmap(cropBitmap, 0, 0, + cropBitmap.getWidth(), cropBitmap.getHeight(), matrix, true); + // 回收原始Bitmap,避免内存泄漏 + if (rotatedBitmap != cropBitmap && !cropBitmap.isRecycled()) { + cropBitmap.recycle(); + } + cropBitmap = rotatedBitmap; + LogUtils.d(TAG, "【裁剪解析】图片旋转纠正:角度=" + rotateAngle + "°,旋转后尺寸:" + cropBitmap.getWidth() + "x" + cropBitmap.getHeight()); + } + } + } catch (OutOfMemoryError e) { + LogUtils.e(TAG, "【裁剪解析】OOM异常:" + e.getMessage()); + ToastUtils.show("图片解析失败(内存不足)"); + // 强制回收内存 + System.gc(); + } catch (Exception e) { + LogUtils.e(TAG, "【裁剪解析】Bitmap加载异常:" + e.getMessage(), e); + } - /** - * 保存裁剪后的Bitmap(调用工具类保存,Java7 语法) - */ - private void saveCropBitmap(Bitmap bitmap) { - LogUtils.d(TAG, "【保存启动】开始保存裁剪图片(仅更新预览Bean,不影响正式Bean)"); - if (bitmap == null || bitmap.isRecycled()) { - ToastUtils.show("裁剪图片为空"); - mBgSourceUtils.clearCropTempFiles(); - return; - } + return cropBitmap; + } - // 内存优化:大图片缩放(保留原有逻辑,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"); - } +// ======================================== 剩余辅助方法(承接前文,保证代码完整性) ======================================== + /** + * 辅助函数:计算最大公约数(用于裁剪比例计算) + */ + private int calculateGCD(int a, int b) { + if (b == 0) return a; + return calculateGCD(b, a % b); + } - // 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(); + /** + * 辅助函数:缩放Bitmap(内存优化用) + */ + private Bitmap scaleBitmap(Bitmap bitmap, float scale) { + if (bitmap == null || bitmap.isRecycled() || scale <= 0) { + return null; + } + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + Bitmap scaledBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + LogUtils.d(TAG, "【内存优化】Bitmap缩放:原始尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight() + + ",缩放比例=" + scale + ",缩放后尺寸=" + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight()); + return scaledBitmap; + } - // 写入文件(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(); - } - } - - LogUtils.d(TAG, "【裁剪保存】预览图保存成功:" + cropSaveFile.getAbsolutePath() + ",大小=" + cropSaveFile.length() + "bytes"); - - // 2. 同步更新预览Bean(核心:仅操作预览Bean,不修改正式Bean) - mBgSourceUtils.saveFileToPreviewBean(cropSaveFile, "裁剪后图片"); - // 3. 生成压缩预览图(确保预览时加载缩略图,提升性能) - compressQualityToRecivedPicture(scaledBitmap); - - 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. 清理裁剪临时文件(调用工具类,统一清理) - mBgSourceUtils.clearCropTempFiles(); - LogUtils.d(TAG, "【裁剪保存】流程结束,临时文件已清理"); - } - - /** - * 辅助函数:缩放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; - } - - /** - * 双重刷新预览(适配MIUI渲染延迟,确保裁剪后立即显示,Java7 语法) - */ - private void doubleRefreshPreview() { - LogUtils.d(TAG, "【预览刷新】触发双重刷新(适配MIUI)"); - // 首次刷新(立即执行) - bvPreviewBackground.reloadPreviewBackground(); - // 延迟刷新(解决MIUI渲染延迟,Java7 Handler+匿名内部类) - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + /** + * 双重刷新预览(适配MIUI裁剪后渲染延迟问题) + */ + private void doubleRefreshPreview() { + // 第一次立即刷新 + bvPreviewBackground.reloadPreviewBackground(); + // 第二次延迟100ms刷新(解决MIUI界面不更新问题) + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】双重刷新完成"); + if (!isFinishing()) { + bvPreviewBackground.reloadPreviewBackground(); + setBackgroundColor(); + LogUtils.d(TAG, "【预览刷新】双重刷新完成(适配MIUI渲染延迟)"); + } } - }, 300); // 300ms延迟,适配大多数机型 - } + }, 100); + } - /** - * 检查图片类型是否为图片(辅助方法,Java7 语法) - */ - private boolean isImageType(String type) { - if (TextUtils.isEmpty(type)) { - return false; - } - return type.startsWith("image/"); - } + /** + * 检查是否为图片类型(处理分享意图用) + */ + private boolean isImageType(String type) { + 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, "【颜色设置】背景颜色重置为透明"); - } - } + /** + * 设置背景颜色(像素拾取后更新用) + */ + private void setBackgroundColor() { + BackgroundBean formalBean = mBgSourceUtils.getCurrentBackgroundBean(); + int pixelColor = formalBean.getPixelColor(); + // 同步预览Bean的颜色值(关键优化:确保旋转恢复后颜色一致) + mBgSourceUtils.getPreviewBackgroundBean().setPixelColor(pixelColor); + bvPreviewBackground.setPixelColor(pixelColor); + LogUtils.d(TAG, "【颜色更新】背景颜色已设置:" + Integer.toHexString(pixelColor)); + } - /** - * 检查并申请存储权限(适配所有Android版本,移除READ_MEDIA_IMAGES,Java7 语法) - */ - private boolean checkAndRequestStoragePermission() { - LogUtils.d(TAG, "【权限检查】开始检查存储权限,Android版本:" + Build.VERSION.SDK_INT); - List needPermissions = new ArrayList(); + @Override + protected void onDestroy() { + super.onDestroy(); + // 清理临时文件(避免残留) + mBgSourceUtils.clearCropTempFiles(); + // 额外清理拍照临时文件(兜底,防止工具类清理不彻底) + if (mfTakePhoto != null && mfTakePhoto.exists()) { + boolean deleteSuccess = mfTakePhoto.delete(); + LogUtils.d(TAG, "【页面销毁】拍照临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + mfTakePhoto.getAbsolutePath()); + } + // 释放工具类引用(避免内存泄漏) + mBgSourceUtils = null; + mPermissionUtils = null; + // 移除所有延迟任务(防止内存泄漏) + new Handler(Looper.getMainLooper()).removeCallbacksAndMessages(null); + LogUtils.d(TAG, "【页面销毁】BackgroundSettingsActivity 已销毁,临时文件、资源已彻底清理"); + } - // 统一用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; +// ======================================== 缺失的接口实现(保证类完整性) ======================================== + @Override + public void onBackPressed() { + // 退出前校验是否有未保存的预览修改(可选,根据业务需求调整) + if (!TextUtils.isEmpty(preViewFilePath) && !preViewFilePath.equals(mBgSourceUtils.getCurrentBackgroundFilePath())) { + new AlertDialog.Builder(this) + .setTitle("提示") + .setMessage("当前有未保存的背景修改,是否确认退出?") + .setPositiveButton("确认", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + finish(); } - } + }) + .setNegativeButton("取消", null) + .show(); + } else { + super.onBackPressed(); + } + } - if (shouldShowRationale) { - // 未勾选“不再询问”:提示用户授予权限(Java7 匿名内部类) - new AlertDialog.Builder(this) - .setTitle("权限申请") - .setMessage("需要存储权限才能选择/拍照/裁剪图片,请授予权限") - .setPositiveButton("确定", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - checkAndRequestStoragePermission(); - } - }) - .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(); - } - } - } - } +// ======================================== 屏幕旋转时保存临时状态(可选,避免数据丢失) ======================================== + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 保存拍照临时文件路径 + if (mfTakePhoto != null && mfTakePhoto.exists()) { + outState.putString("TAKE_PHOTO_PATH", mfTakePhoto.getAbsolutePath()); + } + // 保存预览图片路径 + if (!TextUtils.isEmpty(preViewFilePath)) { + outState.putString("PREVIEW_FILE_PATH", preViewFilePath); + } + // 保存是否提交设置的标记 + outState.putBoolean("IS_COMMIT_SETTINGS", isCommitSettings); + LogUtils.d(TAG, "【状态保存】已保存临时状态(拍照路径/预览路径/提交标记)"); + } - @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资源由自身生命周期管理"); - } + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + // 恢复拍照临时文件(多包名/路径变更场景适配) + String takePhotoPath = savedInstanceState.getString("TAKE_PHOTO_PATH"); + if (!TextUtils.isEmpty(takePhotoPath)) { + File tempTakePhoto = new File(takePhotoPath); + // 校验文件有效性(避免路径失效导致空指针) + if (tempTakePhoto.exists() && tempTakePhoto.isFile()) { + mfTakePhoto = tempTakePhoto; + LogUtils.d(TAG, "【状态恢复】拍照临时文件恢复成功:" + takePhotoPath + ",文件大小:" + tempTakePhoto.length() + "bytes"); + } else { + // 路径失效时重建临时文件(兜底策略) + File tempDir = new File(App.getTempDirPath()); + mfTakePhoto = new File(tempDir, "TakePhoto.jpg"); + LogUtils.w(TAG, "【状态恢复】拍照临时文件路径失效,重建兜底文件:" + mfTakePhoto.getAbsolutePath()); + } + } + // 恢复预览图片路径(确保预览状态一致) + preViewFilePath = savedInstanceState.getString("PREVIEW_FILE_PATH"); + preViewFileUrl = savedInstanceState.getString("PREVIEW_FILE_URL"); // 补充恢复预览Url(对应前文预览信息) + LogUtils.d(TAG, "【状态恢复】预览信息恢复:路径=" + preViewFilePath + ",Url=" + preViewFileUrl); + // 恢复提交设置标记(避免重复提交) + isCommitSettings = savedInstanceState.getBoolean("IS_COMMIT_SETTINGS", false); + LogUtils.d(TAG, "【状态恢复】提交设置标记恢复:" + isCommitSettings); + + // 关键:恢复预览Bean状态(确保旋转后预览界面与旋转前一致) + if (!TextUtils.isEmpty(preViewFilePath)) { + File previewFile = new File(preViewFilePath); + if (previewFile.exists()) { + mBgSourceUtils.saveFileToPreviewBean(previewFile, preViewFileUrl); + // 恢复后刷新预览(解决旋转后界面空白问题) + if (bvPreviewBackground != null) { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【状态恢复】预览界面已刷新,恢复至旋转前状态"); + } + } else { + LogUtils.e(TAG, "【状态恢复】预览文件不存在,无法恢复预览状态:" + preViewFilePath); + } + } + } } - diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java index 7579160a..f643a48a 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -1,14 +1,23 @@ package cc.winboll.studio.powerbell.utils; import android.content.Context; +import android.graphics.Bitmap; +import android.media.ExifInterface; import android.os.Environment; import android.text.TextUtils; -import cc.winboll.studio.powerbell.BuildConfig; -import cc.winboll.studio.powerbell.model.BackgroundBean; +import android.util.Log; 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.model.BackgroundBean; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; /** * @Author ZhanGSKen @@ -570,7 +579,7 @@ public class BackgroundSourceUtils { } LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成(/Pictures/PowerBell下)"); } - + /** * 适配原调用:mBgSourceUtils.copyFile(new File(""), parentDir) * 核心:复用FileUtils,支持「空源文件→仅创建目标目录」和「正常文件复制」两种场景 @@ -620,5 +629,184 @@ public class BackgroundSourceUtils { return "外部存储目录(非应用私有,权限受限)"; } } + + + + // ======================================== 核心实现:获取图片旋转角度 ======================================== + /** + * 读取图片EXIF信息,获取旋转角度(适配JPEG/PNG等主流格式) + * @param imagePath 图片绝对路径(支持本地文件路径,兼容多包名临时目录) + * @return 旋转角度(0/90/180/270,无旋转返回0) + */ + public int getImageRotateAngle(String imagePath) { + // 1. 入参校验(避免空指针/无效路径) + if (TextUtils.isEmpty(imagePath)) { + Log.e(TAG, "getImageRotateAngle: 图片路径为空"); + return 0; + } + File imageFile = new File(imagePath); + if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { + Log.e(TAG, "getImageRotateAngle: 图片文件无效,路径:" + imagePath); + return 0; + } + + InputStream inputStream = null; + try { + // 2. 读取图片EXIF信息(优先用流读取,避免文件占用) + inputStream = new FileInputStream(imageFile); + ExifInterface exifInterface = new ExifInterface(inputStream); + + // 3. 获取旋转角度标签(兼容不同设备的EXIF字段) + int orientation = exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL + ); + + // 4. 解析旋转角度(标准EXIF角度映射) + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: // 正常/翻转等其他情况,均视为0度 + return 0; + } + } catch (IOException e) { + // 兼容异常场景:如图片无EXIF信息、格式不支持(如WebP) + Log.w(TAG, "getImageRotateAngle: 读取EXIF异常,路径:" + imagePath + ",错误:" + e.getMessage()); + return 0; + } finally { + // 5. 关闭流资源(避免内存泄漏/文件占用) + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.e(TAG, "getImageRotateAngle: 流关闭失败,错误:" + e.getMessage()); + } + } + } + } + + + // ======================================== 图片处理核心方法(压缩/裁剪/保存) ======================================== + /** + * 压缩图片并保存(核心修复:路径非空校验+兜底路径,Java7 手动管理流) + */ + public void compressQualityToRecivedPicture(Bitmap bitmap) { + // 兼容裁剪等旧调用:从工具类获取默认压缩路径,转发至重载函数 + String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath(); + compressQualityToRecivedPicture(bitmap, defaultCompressPath); + } + + /** + * 重载方法:指定路径压缩图片并保存(修复:压缩后同步路径到预览Bean) + * 适配场景:裁剪后生成压缩图,强制绑定路径到预览Bean,避免路径错位 + * @param bitmap 待压缩的Bitmap(裁剪后的缩放图) + * @param targetCompressPath 强制指定的压缩目标路径(从预览Bean获取/生成) + */ + public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) { + 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; + try { + String scaledCompressFilePath = targetCompressPath; + // 兜底:若传入路径为空,生成临时压缩路径(兼容异常场景) + if (TextUtils.isEmpty(scaledCompressFilePath)) { + LogUtils.e(TAG, "【压缩异常】指定路径为空,使用临时兜底路径"); + File tempDir = new File(App.getTempDirPath(), "PreviewCompress"); // 多包名环境下临时目录隔离 + if (!tempDir.exists()) { + tempDir.mkdirs(); + FileUtils.copyFile(new File(""), tempDir); // 复用原有目录创建逻辑 + } + scaledCompressFilePath = new File(tempDir, "preview_compress_" + System.currentTimeMillis() + ".jpg").getAbsolutePath(); + LogUtils.d(TAG, "【压缩兜底】临时路径:" + scaledCompressFilePath); + } + + File compressFile = new File(scaledCompressFilePath); + LogUtils.d(TAG, "【压缩配置】目标路径:" + scaledCompressFilePath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); + + // 确保父目录存在(兼容Android 10+分区存储,多包名路径适配) + File parentDir = compressFile.getParentFile(); + if (parentDir == null) { + LogUtils.e(TAG, "【压缩异常】父目录为空,无法创建文件"); + ToastUtils.show("压缩失败:路径无效"); + return; + } + if (!parentDir.exists()) { + parentDir.mkdirs(); + FileUtils.copyFile(new File(""), parentDir); + LogUtils.d(TAG, "【压缩准备】目录已创建:" + parentDir.getAbsolutePath()); + } + + // 清理旧的压缩文件(避免文件残留) + if (compressFile.exists()) { + clearOldFileByExternal(compressFile, "旧压缩文件"); + } + compressFile.createNewFile(); + + // 写入压缩图(质量80,平衡清晰度和内存) + fos = new FileOutputStream(compressFile); + outStream = new BufferedOutputStream(fos); + boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); + outStream.flush(); + // 强制同步到磁盘(避免异步写入导致控件读取不到文件) + if (fos != null) { + try { + fos.getFD().sync(); + LogUtils.d(TAG, "【压缩保存】已强制同步到磁盘"); + } catch (IOException e) { + LogUtils.w(TAG, "【压缩保存】sync()失败,flush()兜底:" + e.getMessage()); + outStream.flush(); + } + } + + LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + compressFile.length() / 1024 + "KB"); + + // 关键修复:压缩成功后,强制同步路径到预览Bean(双重保障,避免时序错位) + if (compressSuccess) { + BackgroundBean previewBean = getPreviewBackgroundBean(); + if (previewBean != null) { + previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath); + saveSettings(); // 持久化配置,多包名环境下配置隔离 + LogUtils.d(TAG, "【压缩路径同步】已绑定到预览Bean:" + scaledCompressFilePath); + } else { + LogUtils.e(TAG, "【压缩路径同步失败】预览Bean为空"); + } + } else { + ToastUtils.show("图片压缩失败"); + LogUtils.e(TAG, "【压缩失败】Bitmap.compress()返回false"); + } + } catch (IOException e) { + LogUtils.e(TAG, "【压缩异常】IO错误:" + e.getMessage(), e); + ToastUtils.show("图片压缩失败"); + } finally { + // 资源回收(避免内存泄漏) + if (outStream != null) { + try { + outStream.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()); + } + } + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java new file mode 100644 index 00000000..1c510fbd --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java @@ -0,0 +1,214 @@ +package cc.winboll.studio.powerbell.utils; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.text.TextUtils; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.R; +import java.util.ArrayList; +import java.util.List; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/01 16:05 + * @Describe 权限申请工具类(单例) + * 核心特性: + * 1. 适配全Android版本(6.0+ 动态权限 / 13+ 兼容) + * 2. 支持多包名场景(无硬编码包名) + * 3. 统一权限校验、申请、回调处理 + * 4. 自带用户引导(拒绝权限+不再询问场景) + */ +public class PermissionUtils { + public static final String TAG = "PermissionUtils"; + // 存储权限请求码(与Activity保持一致,避免冲突) + public static final int STORAGE_PERMISSION_REQUEST2 = 100; + public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101; + + // 单例实例(双重校验锁,线程安全) + private static volatile PermissionUtils sInstance; + + // 私有构造(禁止外部实例化) + private PermissionUtils() {} + + /** + * 获取单例实例(适配多包名,无硬编码) + */ + public static PermissionUtils getInstance() { + if (sInstance == null) { + synchronized (PermissionUtils.class) { + if (sInstance == null) { + sInstance = new PermissionUtils(); + } + } + } + return sInstance; + } + + // ======================================== 存储权限核心方法 ======================================== + /** + * 检查并申请存储权限(统一入口,适配全Android版本) + * @param activity 上下文(用于权限申请和弹窗) + * @return true:权限已全部获取;false:需要申请权限 + */ + public boolean checkAndRequestStoragePermission(Activity activity) { + if (activity == null || activity.isFinishing()) { + LogUtils.e(TAG, "【权限检查】Activity为空或已销毁,权限检查失败"); + return false; + } + LogUtils.d(TAG, "【权限检查】开始检查存储权限,Android版本:" + Build.VERSION.SDK_INT); + + // 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(适配所有版本,避免READ_MEDIA_IMAGES找不到符号) + String[] requiredPermissions = { + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + android.Manifest.permission.READ_EXTERNAL_STORAGE + }; + + // 筛选未授予的权限 + List needPermissions = new ArrayList<>(); + for (String permission : requiredPermissions) { + if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + needPermissions.add(permission); + } + } + + // 无权限需要申请:触发动态申请 + if (!needPermissions.isEmpty()) { + String[] permissionsArr = needPermissions.toArray(new String[0]); + ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2); + LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr)); + return false; + } + + // 所有权限已授予 + LogUtils.d(TAG, "【权限检查】存储权限已全部获取"); + return true; + } + + /** + * 处理存储权限申请回调(统一逻辑,无需在Activity中重复编写) + * @param activity 上下文 + * @param requestCode 请求码(匹配STORAGE_PERMISSION_REQUEST) + * @param permissions 申请的权限数组 + * @param grantResults 权限授予结果数组 + * @return true:回调已处理;false:非当前工具类的权限回调 + */ + public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) { + // 过滤非存储权限回调 + if (requestCode != STORAGE_PERMISSION_REQUEST2) { + return false; + } + LogUtils.d(TAG, "【权限回调】处理存储权限回调,requestCode:" + requestCode); + + if (activity == null || activity.isFinishing()) { + LogUtils.e(TAG, "【权限回调】Activity为空或已销毁,回调处理终止"); + return true; + } + + // 校验所有权限是否授予 + boolean allGranted = true; + for (int grantResult : grantResults) { + if (grantResult != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + break; + } + } + + if (allGranted) { + // 全部授予:提示用户重新操作 + ToastUtils.show(activity.getString(R.string.permission_grant_success)); + LogUtils.d(TAG, "【权限回调】所有存储权限已授予"); + } else { + // 部分/全部拒绝:判断是否勾选“不再询问” + boolean shouldShowRationale = false; + for (String permission : permissions) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { + shouldShowRationale = true; + break; + } + } + + if (shouldShowRationale) { + // 未勾选“不再询问”:弹窗引导重新申请 + showPermissionRationaleDialog(activity); + } else { + // 已勾选“不再询问”:引导用户去设置页开启 + showPermissionSettingDialog(activity); + } + LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale); + } + return true; + } + + // ======================================== 辅助方法(私有,封装细节) ======================================== + /** + * 弹窗:未勾选“不再询问”时,提示用户授予权限 + */ + private void showPermissionRationaleDialog(final Activity activity) { + new AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.permission_title)) + .setMessage(activity.getString(R.string.permission_storage_rationale)) + .setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 重新申请权限 + checkAndRequestStoragePermission(activity); + } + }) + .setNegativeButton(activity.getString(R.string.cancel), null) + .show(); + } + + /** + * 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限 + */ + private void showPermissionSettingDialog(final Activity activity) { + new AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.permission_denied_title)) + .setMessage(activity.getString(R.string.permission_storage_setting_guide)) + .setPositiveButton(activity.getString(R.string.go_to_setting), 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", activity.getPackageName(), null); + intent.setData(uri); + activity.startActivity(intent); + } + }) + .setNegativeButton(activity.getString(R.string.cancel), null) + .show(); + } + + // ======================================== 扩展:其他权限方法(可选) ======================================== + /** + * 检查单个权限是否已授予(通用方法,可复用) + */ + public boolean isPermissionGranted(Activity activity, String permission) { + if (activity == null || TextUtils.isEmpty(permission)) { + return false; + } + return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED; + } + + /** + * 申请单个权限(通用方法,可复用) + */ + public void requestSinglePermission(Activity activity, String permission, int requestCode) { + if (activity == null || TextUtils.isEmpty(permission)) { + return; + } + if (!isPermissionGranted(activity, permission)) { + ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode); + } + } +} + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java index 6cfc56f1..f7503d5b 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java @@ -13,6 +13,9 @@ import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import java.io.File; +import android.widget.ImageView.ScaleType; +import android.text.TextUtils; +import androidx.annotation.Nullable; /** * @Author ZhanGSKen&豆包大模型 @@ -97,6 +100,10 @@ public class BackgroundView extends RelativeLayout { this.addView(ivBackground); // 添加到父容器(控件本身) } + public void setPixelColor(int nBackgroundColor) { + ivBackground.setBackgroundColor(nBackgroundColor); + } + /** * 【对外提供】重新加载正式背景图片(从正式Bean获取路径) * 用于:退出预览模式、恢复默认背景、正式背景更新后刷新 @@ -111,31 +118,126 @@ public class BackgroundView extends RelativeLayout { } /** - * 【对外提供】重新加载预览背景图片(从预览Bean获取路径) - * 修复:增加isUseBackgroundFile校验,确保启用背景图时才加载 + * 重新加载预览背景(修复:强制读取预览Bean最新路径+多重有效性校验) + * 作用:控件加载图片时,优先用压缩图路径,失败则用原图路径,均失败则显示默认图 */ public void reloadPreviewBackground() { - LogUtils.d(TAG, "=== 开始重新加载预览背景 ==="); - isPreviewMode = true; // 标记为预览模式 - // 从工具类获取最新预览背景Bean - BackgroundBean previewBean = backgroundSourceUtils.getPreviewBackgroundBean(); - // 修复:若未启用背景图,直接显示透明背景(避免无效加载) - if (!previewBean.isUseBackgroundFile()) { - LogUtils.d(TAG, "预览Bean未启用背景图(isUseBackgroundFile=false),显示透明背景"); - setDefaultTransparentBackground(); + LogUtils.d(TAG, "=== 开始加载预览背景(路径校验版)==="); + // 关键:直接从工具类获取预览Bean最新路径(避免使用缓存路径) + BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(getContext()); + BackgroundBean previewBean = bgSourceUtils.getPreviewBackgroundBean(); + if (previewBean == null) { + LogUtils.e(TAG, "【加载失败】预览Bean为空,显示默认图"); + setDefaultBackground(); return; } - // 优先加载压缩图,无则加载原图(保持原逻辑) - String backgroundPath; - if (previewBean.isUseBackgroundScaledCompressFile()) { - backgroundPath = backgroundSourceUtils.getPreviewBackgroundScaledCompressFilePath(); - } else { - backgroundPath = backgroundSourceUtils.getPreviewBackgroundFilePath(); + + // 1. 优先加载压缩图路径(预览首选,节省内存) + String compressPath = previewBean.getBackgroundScaledCompressFilePath(); + File compressFile = checkFileValidity(compressPath); // 校验文件有效性 + if (compressFile != null) { + loadBitmapAndDisplay(compressPath, "压缩图"); + return; } - // 加载并显示图片 - loadAndSetImageViewBackground(backgroundPath); + + // 2. 兜底加载原图路径(压缩图失败时) + LogUtils.w(TAG, "【压缩图无效】尝试加载原图路径"); + String originalPath = previewBean.getBackgroundFilePath(); + File originalFile = checkFileValidity(originalPath); + if (originalFile != null) { + loadBitmapAndDisplay(originalPath, "原图"); + return; + } + + // 3. 终极兜底:显示默认图(避免白色) + LogUtils.e(TAG, "【加载失败】压缩图和原图均无效,显示默认图"); + setDefaultBackground(); } + /** + * 新增:校验文件有效性(路径非空+文件存在+是文件+大小>100bytes) + * @param filePath 待校验的图片路径 + * @return 有效则返回File对象,无效则返回null + */ + @Nullable + private File checkFileValidity(String filePath) { + if (TextUtils.isEmpty(filePath)) { + LogUtils.w(TAG, "【文件校验】路径为空"); + return null; + } + File file = new File(filePath); + if (!file.exists()) { + LogUtils.w(TAG, "【文件校验】文件不存在:" + filePath); + return null; + } + if (!file.isFile()) { + LogUtils.w(TAG, "【文件校验】不是文件:" + filePath); + return null; + } + if (file.length() <= 100) { + LogUtils.w(TAG, "【文件校验】文件过小(无效):" + filePath + ",大小:" + file.length() + "bytes"); + return null; + } + LogUtils.d(TAG, "【文件校验】有效:" + filePath + ",大小:" + file.length() + "bytes"); + return file; + } + + /** + * 新增:加载Bitmap并显示(统一处理Bitmap有效性) + * @param imagePath 图片路径 + * @param imageType 图片类型(压缩图/原图,用于日志区分) + */ + private void loadBitmapAndDisplay(String imagePath, String imageType) { + Bitmap bitmap = BitmapFactory.decodeFile(imagePath); + // 校验Bitmap有效性(避免宽高为0的无效Bitmap) + if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { + LogUtils.e(TAG, "【" + imageType + "加载失败】Bitmap无效(空/宽高为0)"); + setDefaultBackground(); + return; + } + + // 强制设置ScaleType(避免因缩放导致图片显示异常) + ivBackground.setScaleType(ScaleType.CENTER_CROP); + // 移除默认背景色(避免白色覆盖图片) + setBackgroundColor(getResources().getColor(android.R.color.transparent)); + // 显示图片并调整尺寸(复用原有调整逻辑) + ivBackground.setImageBitmap(bitmap); + adjustImageViewSize(bitmap); // 复用项目中原有尺寸调整方法 + LogUtils.d(TAG, "【" + imageType + "加载成功】宽高:" + bitmap.getWidth() + "x" + bitmap.getHeight()); + } + + /** + * 兜底:无默认图片时,用代码生成透明/纯色背景(避免报错) + */ + private void setDefaultBackground() { + ivBackground.setScaleType(ScaleType.CENTER_CROP); + // 方案1:生成透明背景(推荐,避免白色) + ivBackground.setBackgroundColor(getResources().getColor(android.R.color.transparent)); + // 或 方案2:生成白色背景(根据界面需求选择) + // ivBackground.setBackgroundColor(getResources().getColor(android.R.color.white)); + // 或 方案3:生成灰色背景(更友好,避免纯白/纯黑突兀) + // ivBackground.setBackgroundColor(getResources().getColor(android.R.color.darker_gray)); + + // 可选:设置一个空的Bitmap,避免ImageView显示空白 + Bitmap emptyBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ivBackground.setImageBitmap(emptyBitmap); + LogUtils.d(TAG, "【默认背景】使用代码生成透明背景(无图片资源)"); + } + +// 复用原有尺寸调整方法(若项目中已存在,无需重复添加) + private void adjustImageViewSize(Bitmap bitmap) { + // 此处保留项目中原有逻辑(如根据控件尺寸缩放图片等) + // 示例逻辑(仅供参考): + int viewWidth = getWidth(); + int viewHeight = getHeight(); + if (viewWidth > 0 && viewHeight > 0 && bitmap != null) { + // 计算缩放比例,适配控件尺寸 + float scale = Math.min((float) viewWidth / bitmap.getWidth(), (float) viewHeight / bitmap.getHeight()); + // 后续尺寸调整逻辑... + } + } + + /** * 【对外提供】预览指定路径的临时图片(直接传入路径,不依赖Bean) * 用于:临时预览本地图片、测试图片等场景 @@ -198,6 +300,22 @@ public class BackgroundView extends RelativeLayout { LogUtils.d(TAG, "图片路径:" + imagePath + ",宽高比:" + imageAspectRatio); } + /** + * 【新增工具函数】校验图片路径有效性(路径非空+文件存在+是文件+大小合理) + * 用于:reloadPreviewBackground中压缩图/原始图的前置校验,避免无效加载 + */ + private boolean isImagePathValid(String imagePath) { + if (imagePath == null || imagePath.isEmpty()) { + LogUtils.d(TAG, "图片路径为空,无效"); + return false; + } + File imageFile = new File(imagePath); + // 校验:文件存在 + 是普通文件 + 大小>100字节(避免空文件/损坏文件) + boolean isValid = imageFile.exists() && imageFile.isFile() && imageFile.length() > 100; + LogUtils.d(TAG, "图片路径校验:" + imagePath + ",是否有效:" + isValid + "(大小:" + imageFile.length() + "字节)"); + return isValid; + } + /** * 计算图片原始宽高比(宽/高)→ 控制不拉伸的核心 * @param file 图片文件(非空) diff --git a/powerbell/src/main/res/values/strings.xml b/powerbell/src/main/res/values/strings.xml index 1aa73199..a81cfdd1 100644 --- a/powerbell/src/main/res/values/strings.xml +++ b/powerbell/src/main/res/values/strings.xml @@ -31,4 +31,15 @@ Pixel Picker About The APP >>>Seek 100% Right Is Clean Record.>>> + + + 权限申请 + 权限被拒绝 + 权限获取成功,请重新操作 + 需要存储权限才能选择/拍照/裁剪图片,请授予权限 + 存储权限已被拒绝且勾选“不再询问”,请前往设置页开启权限 + 确定 + 取消 + 去设置 +