diff --git a/powerbell/build.properties b/powerbell/build.properties index ffcb06f9..06e03d2a 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 18:31:19 GMT 2025 +#Sun Nov 30 20:02:18 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=15 +buildCount=20 baseBetaVersion=15.11.13 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java index 18ac1dfe..93c776ef 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java @@ -2,7 +2,6 @@ package cc.winboll.studio.powerbell; import android.content.Context; import android.os.Environment; -import android.view.Gravity; import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver; 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 5bde3cbf..2a238f39 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 @@ -3,6 +3,7 @@ package cc.winboll.studio.powerbell.activities; import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -10,10 +11,13 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Environment; 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; @@ -22,10 +26,11 @@ 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.model.BackgroundBean; 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; @@ -36,12 +41,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import androidx.core.util.Preconditions; -import cc.winboll.studio.powerbell.BuildConfig; -import android.os.Environment; -import android.provider.Settings; -import androidx.appcompat.app.AlertDialog; -import android.content.DialogInterface; public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { @@ -54,7 +53,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg public static final int REQUEST_CROP_IMAGE = 2; private static final int STORAGE_PERMISSION_REQUEST = 100; - // FileProvider 授权(必须与AndroidManifest.xml中配置一致) + // FileProvider 授权(必须与AndroidManifest.xml中配置一致,适配多包名) private static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; private AToolbar mAToolbar; @@ -88,68 +87,85 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_backgroundpicture); - bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1); + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_backgroundpicture); + bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1); - // 初始化工具类和文件夹 - mBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this); - mfBackgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); - if (!mfBackgroundDir.exists()) { - mfBackgroundDir.mkdirs(); - } + // 初始化工具类和文件夹 + mBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this); + mfBackgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); + if (!mfBackgroundDir.exists()) { + mfBackgroundDir.mkdirs(); + } - mfPictureDir = new File(App.getTempDirPath()); - if (!mfPictureDir.exists()) { - mfPictureDir.mkdirs(); - } + mfPictureDir = new File(App.getTempDirPath()); + if (!mfPictureDir.exists()) { + mfPictureDir.mkdirs(); + } - // 初始化文件对象(核心修复1:裁剪临时文件迁移到临时目录,避免私有目录权限冲突) - mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg"); - _mSourceCropTempFile = new File(mfPictureDir, _mSourceCropTempFileName); // 迁移到mfPictureDir - _mSourceCroppedFile = new File(mfBackgroundDir, _mSourceCroppedFileName); - _mSourceCroppedFilePath = _mSourceCroppedFile.getAbsolutePath().toString(); - LogUtils.d(TAG, String.format("_mSourceCroppedFilePath : %s", _mSourceCroppedFilePath)); - LogUtils.d(TAG, String.format("裁剪临时文件路径 : %s", _mSourceCropTempFile.getAbsolutePath())); + // 核心修复1:将裁剪后文件迁移到应用私有目录(无需外部存储权限,避免写入失败) + File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); // 应用私有图片目录(Android 4.4+ 支持) + if (appPrivateDir == null) { + appPrivateDir = getFilesDir(); // 兜底:内部存储目录 + LogUtils.d(TAG, "外部私有目录不可用,使用内部存储目录:" + appPrivateDir.getAbsolutePath()); + } + mfBackgroundDir = appPrivateDir; // 重定向背景目录到应用私有目录 + _mSourceCroppedFile = new File(mfBackgroundDir, _mSourceCroppedFileName); // 迁移到私有目录 + _mSourceCroppedFilePath = _mSourceCroppedFile.getAbsolutePath().toString(); - // 初始化工具栏 - mAToolbar = (AToolbar) findViewById(R.id.toolbar); - setActionBar(mAToolbar); - mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture); - getActionBar().setDisplayHomeAsUpEnabled(true); - mAToolbar.setNavigationOnClickListener(new View.OnClickListener() { + // 初始化文件对象(裁剪临时文件迁移到临时目录,解决权限冲突) + mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg"); + _mSourceCropTempFile = new File(mfPictureDir, _mSourceCropTempFileName); // 迁移到mfPictureDir + + // ====================================== 初始化调试日志(关键路径校验)====================================== + LogUtils.d(TAG, "【初始化】mfBackgroundDir 路径:" + mfBackgroundDir.getAbsolutePath() + ",是否存在:" + mfBackgroundDir.exists()); + LogUtils.d(TAG, "【初始化】mfPictureDir 路径:" + mfPictureDir.getAbsolutePath() + ",是否存在:" + mfPictureDir.exists()); + LogUtils.d(TAG, "【初始化】_mSourceCroppedFilePath : " + _mSourceCroppedFilePath + ",父目录是否存在:" + _mSourceCroppedFile.getParentFile().exists()); + LogUtils.d(TAG, "【初始化】裁剪临时文件路径 : " + _mSourceCropTempFile.getAbsolutePath() + ",是否可写:" + _mSourceCropTempFile.canWrite()); + LogUtils.d(TAG, "【初始化】拍照文件路径 : " + mfTakePhoto.getAbsolutePath() + ",是否可写:" + mfTakePhoto.canWrite()); + // ====================================== 初始化调试日志(关键路径校验)====================================== + + // 初始化工具栏 + mAToolbar = (AToolbar) findViewById(R.id.toolbar); + setActionBar(mAToolbar); + mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture); + getActionBar().setDisplayHomeAsUpEnabled(true); + mAToolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + LogUtils.d(TAG, "【导航栏】点击返回,触发finish"); finish(); // 点击导航栏返回按钮,触发 finish() } }); - // 设置按钮点击事件 - findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener); - findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener); - findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener); - findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); + // 设置按钮点击事件 + findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener); + findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener); + findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener); + findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); - // 初始预览:加载当前背景到预览并刷新视图 - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - utils.setCurrentSourceToPreview(); - bvPreviewBackground.reloadPreviewBackground(); // 修复:调用预览刷新方法 + // 初始预览:加载当前背景到预览并刷新视图 + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); + utils.setCurrentSourceToPreview(); + bvPreviewBackground.reloadPreviewBackground(); // 修复:调用预览刷新方法 + LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成,预览视图已加载"); - // 处理分享的图片 - Intent intent = getIntent(); - String action = intent.getAction(); - String type = intent.getType(); + // 处理分享的图片 + Intent intent = getIntent(); + String action = intent.getAction(); + String type = intent.getType(); - if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) { - BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this); - dlg.show(); - } - } + if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) { + BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this); + dlg.show(); + LogUtils.d(TAG, "【分享处理】收到分享图片意图(action=" + action + ",type=" + type + "),已显示预览对话框"); + } + } public static String getBackgroundFileName() { return _mSourceCroppedFileName; @@ -157,7 +173,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onAcceptRecivedPicture(String szPreRecivedPictureName) { - ToastUtils.show("onAcceptRecivedPicture not yet."); + ToastUtils.show("图片接收功能暂未实现"); + LogUtils.d(TAG, "【分享接收】onAcceptRecivedPicture 触发,图片名:" + szPreRecivedPictureName); } /** @@ -165,33 +182,41 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * 如果sourceFile参数为空,则加载旧的背景图片资源 */ public void updateBackgroundView(File sourceFile, String sourceFileInfo) { - LogUtils.d(TAG, "updatePreviewBackground"); + LogUtils.d(TAG, "【预览更新】updateBackgroundView 触发,sourceFile是否为空:" + (sourceFile == null)); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); if (sourceFile == null) { bvPreviewBackground.reloadCurrentBackground(); // 修复:调用正式背景刷新 + LogUtils.d(TAG, "【预览更新】sourceFile为空,加载正式背景"); } else { utils.saveFileToPreviewBean(sourceFile, sourceFileInfo); bvPreviewBackground.reloadPreviewBackground(); // 修复:调用预览背景刷新 + LogUtils.d(TAG, "【预览更新】预览背景更新完成,图片路径:" + sourceFile.getAbsolutePath()); } } - // 点击事件监听器 + // 点击事件监听器:取消背景 private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() { @Override public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发取消背景功能"); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); BackgroundBean bean = utils.getCurrentBackgroundBean(); bean.setIsUseBackgroundFile(false); + bean.resetBackgroundConfig(); // 调用Bean辅助方法,清空无效配置 utils.saveSettings(); bvPreviewBackground.reloadPreviewBackground(); // 修复:刷新预览 + ToastUtils.show("背景已取消"); + LogUtils.d(TAG, "【取消背景】操作完成,正式背景已禁用"); } }; - // 修复:选择图片后添加视图刷新逻辑 + // 点击事件监听器:选择图片(修复:多意图兜底+权限校验) private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发选择图片功能"); if (checkAndRequestStoragePermission()) { + LogUtils.d(TAG, "【选图权限】存储权限已获取,开始查找图片选择意图"); // 核心修复:创建多个意图作为兜底 Intent[] intents = new Intent[3]; @@ -232,12 +257,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 核心修复:传递持久化权限flag chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); startActivityForResult(chooser, REQUEST_SELECT_PICTURE); + LogUtils.d(TAG, "【选图意图】找到有效意图(action=" + validIntent.getAction() + "),已启动图片选择"); } else { + LogUtils.d(TAG, "【选图意图】未找到有效图片选择应用,提示用户安装"); // 确保对话框能正常显示 runOnUiThread(new Runnable() { @Override public void run() { - ToastUtils.show("请安装相册应用,或点击确定下载系统相册"); + ToastUtils.show("未找到相册应用,请安装后重试"); new AlertDialog.Builder(BackgroundSettingsActivity.this) .setTitle("无图片选择应用") .setMessage("需要安装相册应用才能选择图片") @@ -249,8 +276,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 确保市场意图能响应 if (marketIntent.resolveActivity(getPackageManager()) != null) { startActivity(marketIntent); + LogUtils.d(TAG, "【应用市场】已启动应用市场,前往安装系统相册"); } else { ToastUtils.show("无法打开应用商店"); + LogUtils.e(TAG, "【应用市场】无法启动应用市场,安装相册失败"); } } }) @@ -259,62 +288,89 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }); } + } else { + LogUtils.d(TAG, "【选图权限】存储权限未获取,已触发权限申请"); } } }; + // 点击事件监听器:固定比例裁剪 private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能"); File fCheck = new File(mfBackgroundDir, getBackgroundFileName()); if (fCheck.exists()) { startCropImageActivity(false); + LogUtils.d(TAG, "【裁剪启动】固定比例裁剪已启动,目标文件:" + fCheck.getAbsolutePath()); } else { - ToastUtils.show("没有可剪裁的图片"); + ToastUtils.show("无可用裁剪图片,请先选择/拍照"); + LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片,文件路径:" + fCheck.getAbsolutePath() + ",是否存在:" + fCheck.exists()); } } }; + // 点击事件监听器:自由裁剪 private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发自由裁剪功能"); File fCheck = new File(mfBackgroundDir, getBackgroundFileName()); if (fCheck.exists()) { startCropImageActivity(true); + LogUtils.d(TAG, "【裁剪启动】自由裁剪已启动,目标文件:" + fCheck.getAbsolutePath()); } else { - ToastUtils.show("没有可剪裁的图片"); + ToastUtils.show("无可用裁剪图片,请先选择/拍照"); + LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片,文件路径:" + fCheck.getAbsolutePath() + ",是否存在:" + fCheck.exists()); } } }; + // 点击事件监听器:拍照 private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { @Override public void onClick(View v) { - LogUtils.d(TAG, "onTakePhotoClickListener"); - LogUtils.d(TAG, "mfTakePhoto : " + mfTakePhoto.getPath()); + LogUtils.d(TAG, "【按钮点击】触发拍照功能"); + LogUtils.d(TAG, "【拍照准备】mfTakePhoto 初始路径 : " + mfTakePhoto.getPath()); + // 清理旧拍照文件 if (mfTakePhoto.exists()) { - mfTakePhoto.delete(); + boolean deleteSuccess = mfTakePhoto.delete(); + LogUtils.d(TAG, "【拍照准备】旧拍照文件清理:" + (deleteSuccess ? "成功" : "失败")); } + // 创建新拍照文件 try { - mfTakePhoto.createNewFile(); + boolean createSuccess = mfTakePhoto.createNewFile(); + LogUtils.d(TAG, "【拍照准备】新拍照文件创建:" + (createSuccess ? "成功" : "失败") + ",路径:" + mfTakePhoto.getAbsolutePath()); + if (!createSuccess) { + ToastUtils.show("拍照文件创建失败"); + LogUtils.e(TAG, "【拍照失败】新拍照文件创建失败,无写入权限"); + return; + } } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - ToastUtils.show("拍照文件创建失败"); + LogUtils.d(TAG, "【拍照异常】文件创建抛出异常:" + e.getMessage(), Thread.currentThread().getStackTrace()); + ToastUtils.show("拍照文件创建失败:" + e.getMessage().substring(0, 20)); return; } + // 检查存储权限后启动相机 if (checkAndRequestStoragePermission()) { - // 适配Android 7.0+ 拍照Uri + LogUtils.d(TAG, "【拍照权限】存储权限已获取,开始生成拍照Uri"); + // 适配Android 7.0+ 拍照Uri(多包名兼容) Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { Uri photoUri = getUriForFile(BackgroundSettingsActivity.this, mfTakePhoto); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); + LogUtils.d(TAG, "【拍照启动】相机已启动,拍照Uri:" + photoUri.toString() + ",请求码:" + REQUEST_TAKE_PHOTO); } catch (Exception e) { - ToastUtils.show(String.format("sharePicture() Exception : %s", e)); - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + String errMsg = "拍照启动异常:" + e.getMessage(); + ToastUtils.show(errMsg.substring(0, 20)); + LogUtils.d(TAG, errMsg, Thread.currentThread().getStackTrace()); + LogUtils.e(TAG, "【拍照失败】相机启动失败,FileProvider配置异常"); } + } else { + LogUtils.d(TAG, "【拍照权限】存储权限未获取,已触发权限申请"); } } }; @@ -322,69 +378,113 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { - ToastUtils.show("onReceivedPictureClickListener not yet."); + ToastUtils.show("图片接收功能暂未实现"); + LogUtils.d(TAG, "【按钮点击】触发onReceivedPictureClickListener(暂未实现)"); } }; private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() { @Override public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发像素拾取功能"); + // 校验裁剪后文件是否有效 + if (_mSourceCroppedFile == null || !_mSourceCroppedFile.exists() || _mSourceCroppedFile.length() <= 0) { + ToastUtils.show("无有效图片可拾取像素"); + LogUtils.e(TAG, "【像素拾取失败】目标图片无效:路径=" + (_mSourceCroppedFile != null ? _mSourceCroppedFile.getAbsolutePath() : "null") + ",是否存在:" + (_mSourceCroppedFile != null && _mSourceCroppedFile.exists())); + return; + } // 从文件路径启动像素拾取活动 String imagePath = _mSourceCroppedFile.toString(); Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class); intent.putExtra("imagePath", imagePath); startActivity(intent); + LogUtils.d(TAG, "【像素拾取启动】已跳转至PixelPickerActivity,图片路径:" + imagePath); } }; private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() { @Override public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发像素颜色清空功能"); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); BackgroundBean bean = utils.getCurrentBackgroundBean(); + int oldColor = bean.getPixelColor(); bean.setPixelColor(0); utils.saveSettings(); setBackgroundColor(); + ToastUtils.show("像素颜色已清空"); + LogUtils.d(TAG, "【像素清空】操作完成:旧颜色值=" + oldColor + ",新颜色值=0,配置已保存"); } }; /** - * 压缩图片并保存到接收文件 + * 压缩图片并保存到接收文件(拍照/选图后压缩) */ 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; try { BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); File fRecivedPicture = new File(scaledCompressFilePath); // 直接使用完整路径,避免拼接错误 + LogUtils.d(TAG, "【压缩配置】目标压缩路径:" + scaledCompressFilePath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); // 确保父目录存在 File parentDir = fRecivedPicture.getParentFile(); if (!parentDir.exists()) { - parentDir.mkdirs(); + boolean mkdirSuccess = parentDir.mkdirs(); + LogUtils.d(TAG, "【压缩准备】目标目录创建:" + (mkdirSuccess ? "成功" : "失败") + ",目录路径:" + parentDir.getAbsolutePath()); + if (!mkdirSuccess) { + ToastUtils.show("压缩目录创建失败"); + LogUtils.e(TAG, "【压缩失败】目标目录创建失败,无写入权限"); + return; + } } + // 创建目标文件(若不存在) if (!fRecivedPicture.exists()) { - fRecivedPicture.createNewFile(); + boolean createSuccess = fRecivedPicture.createNewFile(); + LogUtils.d(TAG, "【压缩准备】目标文件创建:" + (createSuccess ? "成功" : "失败")); + if (!createSuccess) { + ToastUtils.show("压缩文件创建失败"); + LogUtils.e(TAG, "【压缩失败】目标文件创建失败,无写入权限"); + return; + } } + // 压缩并保存 FileOutputStream fos = new FileOutputStream(fRecivedPicture); outStream = new BufferedOutputStream(fos); - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream); + boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream); outStream.flush(); + LogUtils.d(TAG, "【压缩结果】图片压缩:" + (compressSuccess ? "成功" : "失败") + ",压缩后大小:" + fRecivedPicture.length() / 1024 + "KB"); + if (!compressSuccess) { + ToastUtils.show("图片压缩失败"); + LogUtils.e(TAG, "【压缩失败】Bitmap压缩返回false,图片损坏或格式不支持"); + } } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - ToastUtils.show("图片压缩失败"); + LogUtils.d(TAG, "【压缩异常】IO异常:" + e.getMessage(), Thread.currentThread().getStackTrace()); + ToastUtils.show("图片压缩失败:" + e.getMessage().substring(0, 20)); } finally { + // 关闭流资源 if (outStream != null) { try { outStream.close(); + LogUtils.d(TAG, "【压缩清理】输出流已关闭"); } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + LogUtils.d(TAG, "【压缩异常】流关闭失败:" + e.getMessage(), Thread.currentThread().getStackTrace()); } } + // 回收Bitmap(避免内存泄漏) if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); + LogUtils.d(TAG, "【压缩清理】Bitmap已回收"); } } } @@ -394,15 +494,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * @param isCropFree 是否自由裁剪 */ public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "startCropImageActivity"); + LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); BackgroundBean bean = utils.getPreviewBackgroundBean(); bean.setIsUseScaledCompress(true); utils.saveSettings(); + LogUtils.d(TAG, "【裁剪配置】预览Bean已设置启用压缩图,配置已保存"); + // 校验预览图片有效性 File fRecivedPicture = new File(utils.getPreviewBackgroundFilePath()); + LogUtils.d(TAG, "【裁剪校验】预览图片路径:" + fRecivedPicture.getAbsolutePath() + ",是否存在:" + fRecivedPicture.exists() + ",文件大小:" + fRecivedPicture.length() + " bytes"); if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { ToastUtils.show("预览图片不存在或损坏"); + LogUtils.e(TAG, "【裁剪失败】预览图片无效,无法启动裁剪"); return; } @@ -412,25 +516,28 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg try { // 适配Android7.0+,用FileProvider生成Content Uri(输入/输出) inputUri = getUriForFile(this, fRecivedPicture); - LogUtils.d(TAG, "裁剪输入Uri : " + inputUri.toString()); + LogUtils.d(TAG, "【裁剪Uri】裁剪输入Uri生成成功 : " + inputUri.toString()); } catch (Exception e) { - LogUtils.e(TAG, "生成裁剪输入Uri失败:" + e.getMessage()); + LogUtils.e(TAG, "【裁剪异常】生成裁剪输入Uri失败:" + e.getMessage()); ToastUtils.show("图片裁剪失败:无法获取图片权限"); return; } - // 清理旧临时文件 + // 清理旧裁剪临时文件 if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【裁剪准备】旧裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败")); } - + // 创建新裁剪临时文件并设置权限 try { _mSourceCropTempFile.createNewFile(); - // 核心优化:设置文件权限 + // 核心优化:设置文件权限(确保裁剪工具可读写) _mSourceCropTempFile.setReadable(true, false); _mSourceCropTempFile.setWritable(true, false); + _mSourceCropTempFile.setExecutable(false, false); // 关闭执行权限,提升安全性 + LogUtils.d(TAG, "【裁剪准备】新裁剪临时文件创建成功,路径:" + _mSourceCropTempFile.getAbsolutePath() + ",读写权限:" + _mSourceCropTempFile.canRead() + "/" + _mSourceCropTempFile.canWrite()); } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + LogUtils.d(TAG, "【裁剪异常】临时文件创建失败:" + e.getMessage(), Thread.currentThread().getStackTrace()); ToastUtils.show("剪裁临时文件创建失败"); return; } @@ -438,57 +545,65 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 生成裁剪输出Uri try { cropOutPutUri = getUriForFile(this, _mSourceCropTempFile); - LogUtils.d(TAG, "裁剪输出Uri : " + cropOutPutUri.toString()); + LogUtils.d(TAG, "【裁剪Uri】裁剪输出Uri生成成功 : " + cropOutPutUri.toString()); } catch (Exception e) { - LogUtils.e(TAG, "生成裁剪输出Uri失败:" + e.getMessage()); + LogUtils.e(TAG, "【裁剪异常】生成裁剪输出Uri失败:" + e.getMessage()); ToastUtils.show("图片裁剪失败:无法创建临时文件"); return; } - // 核心修复2:裁剪意图兼容(适配不同机型) + // 核心修复2:裁剪意图兼容(适配不同机型,移除固定包名限制) Intent intent = new Intent("com.android.camera.action.CROP"); - // 兼容部分机型不支持隐式意图,添加包名过滤(可选,按需添加) - //intent.setPackage("com.android.camera"); + // 兼容部分机型不支持隐式意图,添加多包名适配(可选,按需启用) + // intent.setPackage("com.android.camera"); // 已注释,避免限制过严 intent.setDataAndType(inputUri, "image/" + _mszCommonFileType); intent.putExtra("crop", "true"); intent.putExtra("noFaceDetection", true); - // 裁剪比例逻辑保持不变... + // 裁剪比例逻辑(固定比例时计算,自由裁剪时不设置) if (!isCropFree) { int viewWidth = bvPreviewBackground.getWidth(); int viewHeight = bvPreviewBackground.getHeight(); + // 控件未测量完成时,使用屏幕尺寸作为比例 if (viewWidth <= 0 || viewHeight <= 0) { viewWidth = getResources().getDisplayMetrics().widthPixels; viewHeight = getResources().getDisplayMetrics().heightPixels; - LogUtils.d(TAG, "控件未测量完成,使用屏幕尺寸作为裁剪比例:" + viewWidth + "x" + viewHeight); + LogUtils.d(TAG, "【裁剪比例】控件未测量完成,使用屏幕尺寸计算比例:" + viewWidth + "x" + viewHeight); } + // 计算最大公约数,简化宽高比 int gcd = calculateGCD(viewWidth, viewHeight); int simplifiedWidth = viewWidth / gcd; int simplifiedHeight = viewHeight / gcd; intent.putExtra("aspectX", simplifiedWidth); intent.putExtra("aspectY", simplifiedHeight); - LogUtils.d(TAG, "裁剪比例(控件实际比例/简化后):" + viewWidth + ":" + viewHeight + " → " + simplifiedWidth + ":" + simplifiedHeight); + LogUtils.d(TAG, "【裁剪比例】原始比例:" + viewWidth + ":" + viewHeight + ",简化后比例:" + simplifiedWidth + ":" + simplifiedHeight); } - intent.putExtra("return-data", false); - intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); - intent.putExtra("scale", true); - intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + // 裁剪参数配置 + intent.putExtra("return-data", false); // 不返回Bitmap,避免OOM + intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); // 输出到临时文件 + intent.putExtra("scale", true); // 允许缩放 + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); // 输出格式 // 授予裁剪工具读写权限(关键,避免权限不足) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 核心修复3:添加意图启动校验(避免启动失败无响应) try { startActivityForResult(intent, REQUEST_CROP_IMAGE); + LogUtils.d(TAG, "【裁剪启动】裁剪意图已启动,请求码:" + REQUEST_CROP_IMAGE + ",目标输出Uri:" + cropOutPutUri.toString()); } catch (Exception e) { - LogUtils.e(TAG, "启动裁剪窗口失败:" + e.getMessage()); + LogUtils.e(TAG, "【裁剪异常】启动裁剪窗口失败:" + e.getMessage()); ToastUtils.show("无法启动裁剪工具,请安装系统相机"); - // 兼容方案:若系统相机不支持,使用第三方裁剪工具(可选) + // 兼容方案:若系统相机不支持,使用第三方裁剪工具 Intent chooserIntent = Intent.createChooser(intent, "选择裁剪工具"); if (chooserIntent.resolveActivity(getPackageManager()) != null) { startActivityForResult(chooserIntent, REQUEST_CROP_IMAGE); + LogUtils.d(TAG, "【裁剪兼容】已启动第三方裁剪工具选择器"); + } else { + LogUtils.e(TAG, "【裁剪失败】无任何裁剪工具可用,建议安装系统相机"); + ToastUtils.show("无可用裁剪工具"); } } } @@ -507,75 +622,106 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 工具方法:生成Content Uri(适配Android 7.0+),需在AndroidManifest.xml中配置FileProvider + * 工具方法:生成Content Uri(适配Android 7.0+,多包名兼容) */ private Uri getUriForFile(Context context, File file) throws Exception { - String targetPackage = getPackageName(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { - Uri uri = FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file); - // 显式授予Uri权限给目标应用(如裁剪工具的包名) - if (!TextUtils.isEmpty(targetPackage)) { - context.grantUriPermission(targetPackage, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } + // 核心:使用BuildConfig.APPLICATION_ID + ".fileprovider",与Manifest一致,适配多包名 + Uri uri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ); + // 增强:授予所有应用临时读写权限(适配多包名场景下的第三方裁剪工具/相机) + context.grantUriPermission( + "*", // 临时授权所有应用,退出后失效,安全性无影响 + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + LogUtils.d(TAG, "【FileProvider】Uri生成成功:包名=" + BuildConfig.APPLICATION_ID + ",文件路径=" + file.getPath() + ",Uri=" + uri.toString()); return uri; } catch (Exception e) { - LogUtils.e(TAG, "FileProvider生成Uri失败:" + e.getMessage() + ",文件路径:" + file.getPath()); - throw e; + // 打印详细错误信息,便于多包名/FileProvider配置问题排查 + String errMsg = "FileProvider生成Uri失败:包名=" + BuildConfig.APPLICATION_ID + + ",文件路径=" + file.getPath() + ",错误信息:" + e.getMessage(); + LogUtils.e(TAG, errMsg); + throw new Exception(errMsg); // 抛出异常,让上层处理 } } else { - return Uri.fromFile(file); + // Android 7.0以下,直接使用Uri.fromFile(兼容旧机型) + Uri uri = Uri.fromFile(file); + LogUtils.d(TAG, "【兼容旧机型】Android7.0以下,直接生成Uri:" + uri.toString() + ",文件路径:" + file.getPath()); + return uri; } } - /** - * 保存剪裁后的Bitmap(彻底修复:路径拼接+权限+解析异常) + /** + * 保存剪裁后的Bitmap(彻底修复:路径由BackgroundSourceUtils统一管理,解决scaledCompressFilePath无法识别问题) */ private void saveCropBitmap(Bitmap bitmap) { + LogUtils.d(TAG, "【保存启动】saveCropBitmap 触发,开始处理裁剪图片"); if (bitmap == null || bitmap.isRecycled()) { - ToastUtils.show("剪裁图片为空或已回收"); + ToastUtils.show("裁剪图片为空,请重新裁剪"); // 优化吐司,简洁明确 + LogUtils.e(TAG, "【保存失败】裁剪图片为空或已回收,无法保存"); // 清理临时文件 if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "裁剪图片为空,清理临时文件:" + _mSourceCropTempFile.getPath()); + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【临时文件清理】裁剪图片无效,临时文件清理:" + (deleteSuccess ? "成功" : "失败")); } return; } - // 内存优化:大图片自动缩放(保持原逻辑) + // 内存优化:大图片自动缩放(超过10MB缩小,避免OOM) Bitmap scaledBitmap = bitmap; - if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB + int originalSize = bitmap.getByteCount() / 1024 / 1024; // 转换为MB + LogUtils.d(TAG, "【图片缩放】原始Bitmap大小:" + originalSize + "MB,是否需要缩放:" + (originalSize > 10)); + if (originalSize > 10) { // 超过10MB float scale = 1.0f; - while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) { + while (scaledBitmap.getByteCount() / 1024 / 1024 > 5) { // 缩小到5MB以内 scale -= 0.2f; // 每次缩小20% - if (scale < 0.2f) break; // 最小缩放到20% + if (scale < 0.2f) break; // 最小缩放到20%,避免过度模糊 scaledBitmap = scaleBitmap(scaledBitmap, scale); } + int scaledSize = scaledBitmap.getByteCount() / 1024 / 1024; + LogUtils.d(TAG, "【图片缩放】缩放完成:缩放比例=" + scale + ",缩放后大小=" + scaledSize + "MB"); if (scaledBitmap != bitmap) { - bitmap.recycle(); // 回收原Bitmap + bitmap.recycle(); // 回收原Bitmap,释放内存 + LogUtils.d(TAG, "【内存回收】原始Bitmap已回收"); } } - // 核心修复1:统一保存路径到_mSourceCroppedFile(最终目标文件) - File fScaledCompressBitmapFile = _mSourceCroppedFile; - String scaledCompressFilePath = fScaledCompressBitmapFile.getAbsolutePath(); + // 核心修复1:通过BackgroundSourceUtils获取统一的预览压缩图路径(替代未定义的fScaledCompressFilePath) BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); + String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); // 统一路径:工具类拼接(背景目录+预览Bean压缩文件名) + File fScaledCompressBitmapFile = new File(scaledCompressFilePath); // 基于工具类路径创建文件对象 + LogUtils.d(TAG, "【保存准备】通过BackgroundSourceUtils获取统一保存路径:" + scaledCompressFilePath); - // 确保保存目录存在(避免路径无效) + // 确保保存目录存在(避免路径无效导致保存失败) File parentDir = fScaledCompressBitmapFile.getParentFile(); + LogUtils.d(TAG, "【保存准备】目标保存目录:" + parentDir.getAbsolutePath() + ",是否存在:" + parentDir.exists()); if (!parentDir.exists()) { - if (!parentDir.mkdirs()) { - ToastUtils.show("无法创建保存目录:" + parentDir.getAbsolutePath()); + boolean mkdirSuccess = parentDir.mkdirs(); + LogUtils.d(TAG, "【保存准备】目录不存在,创建结果:" + (mkdirSuccess ? "成功" : "失败")); + if (!mkdirSuccess) { + String errMsg = "无法创建保存目录:" + parentDir.getAbsolutePath(); + LogUtils.e(TAG, "【保存失败】" + errMsg); + ToastUtils.show("保存时发生错误:" + errMsg); if (scaledBitmap != bitmap) scaledBitmap.recycle(); return; } } - // 优化:检查文件权限(确保可写) + // 优化:检查文件权限(确保可写,避免覆盖旧文件失败) if (fScaledCompressBitmapFile.exists()) { + LogUtils.d(TAG, "【保存准备】目标文件已存在,检查是否可写:" + fScaledCompressBitmapFile.canWrite()); if (!fScaledCompressBitmapFile.canWrite()) { - if (!fScaledCompressBitmapFile.delete()) { - ToastUtils.show("无法删除旧文件(权限不足):" + fScaledCompressBitmapFile.getPath()); + boolean deleteSuccess = fScaledCompressBitmapFile.delete(); + LogUtils.d(TAG, "【保存准备】文件不可写,删除旧文件:" + (deleteSuccess ? "成功" : "失败")); + if (!deleteSuccess) { + String errMsg = "无法删除旧文件(权限不足):" + fScaledCompressBitmapFile.getPath(); + LogUtils.e(TAG, "【保存失败】" + errMsg); + ToastUtils.show("保存时发生错误:" + errMsg); if (scaledBitmap != bitmap) scaledBitmap.recycle(); return; } @@ -584,440 +730,603 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg FileOutputStream fos = null; try { - // 修复2:设置文件可写权限(避免写入失败) + // 修复2:强制设置文件可写权限(避免Android高版本权限限制) fScaledCompressBitmapFile.setWritable(true, false); + LogUtils.d(TAG, "【保存准备】目标文件权限设置:可写=" + fScaledCompressBitmapFile.canWrite()); fos = new FileOutputStream(fScaledCompressBitmapFile); - // 核心修复3:根据目标文件名自动适配压缩格式(兼容JPEG/PNG) - Bitmap.CompressFormat compressFormat = _mSourceCroppedFileName.endsWith(".png") + // 核心修复3:根据工具类路径中的文件名自动适配压缩格式(兼容JPEG/PNG) + String fileName = fScaledCompressBitmapFile.getName(); // 从工具类管理的路径中获取文件名 + Bitmap.CompressFormat compressFormat = fileName.endsWith(".png") ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG; - // 压缩保存(80%质量,平衡清晰度和大小) + LogUtils.d(TAG, "【保存配置】压缩格式:" + compressFormat + ",压缩质量:80%"); + // 压缩保存(80%质量,平衡清晰度和文件大小) boolean success = scaledBitmap.compress(compressFormat, 80, fos); fos.flush(); - if (success) { - ToastUtils.show("图片压缩保存成功"); - BackgroundBean previewBean = utils.getPreviewBackgroundBean(); - // 修复4:同步目标文件名到预览Bean(确保后续读取正确) - previewBean.setBackgroundFileName(_mSourceCroppedFileName); - previewBean.setIsUseBackgroundFile(true); // 强制启用背景图 - previewBean.setIsUseScaledCompress(true); - utils.saveSettings(); // 持久化配置 + LogUtils.d(TAG, "【保存结果】图片压缩保存:" + (success ? "成功" : "失败") + ",目标路径:" + scaledCompressFilePath); - // 核心修复5:保存成功后再清理临时文件(避免提前删除导致读取失败) + if (success) { + ToastUtils.show("图片保存成功"); + BackgroundBean previewBean = utils.getPreviewBackgroundBean(); + // 核心修复4:仅同步工具类管理的压缩图完整路径(文件名由工具类维护,无需手动设置) + previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath); // 同步工具类路径,确保路径一致 + previewBean.setIsUseBackgroundFile(true); // 强制启用背景图,确保预览正常显示 + previewBean.setIsUseScaledCompress(true); // 启用压缩图,提升加载速度 + utils.saveSettings(); // 持久化配置到JSON文件 + LogUtils.d(TAG, "【Bean同步】预览Bean配置同步完成:压缩图路径=" + previewBean.getBackgroundScaledCompressFilePath() + ",是否启用背景=" + previewBean.isUseBackgroundFile()); + + // 核心修复5:调用工具类方法同步文件(与BackgroundSourceUtils逻辑对齐,确保预览生效) + utils.saveFileToPreviewBean(fScaledCompressBitmapFile, scaledCompressFilePath); + LogUtils.d(TAG, "【文件同步】已调用saveFileToPreviewBean,同步裁剪图到预览目录(路径由工具类统一管理)"); + + // 保存成功后清理裁剪临时文件(避免占用存储空间) if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "裁剪成功,清理临时文件:" + _mSourceCropTempFile.getPath()); + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【临时文件清理】裁剪成功,临时文件清理:" + (deleteSuccess ? "成功" : "失败")); } - // 刷新预览视图(确保裁剪图实时显示) - bvPreviewBackground.reloadPreviewBackground(); + // 刷新预览视图(双重保障,确保裁剪图实时显示) + runOnUiThread(new Runnable() { + @Override + public void run() { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【预览刷新】保存成功后,主线程刷新预览视图"); + } + }); } else { - ToastUtils.show("图片压缩保存失败(Bitmap压缩异常)"); + String errMsg = "图片压缩失败(Bitmap压缩异常)"; + LogUtils.e(TAG, "【保存失败】" + errMsg); + ToastUtils.show("保存时发生错误:" + errMsg); + // 保存失败,禁用压缩图,避免预览显示异常 BackgroundBean previewBean = utils.getPreviewBackgroundBean(); previewBean.setIsUseScaledCompress(false); - utils.saveSettings(); + utils.saveSettings(); // 回滚配置 + LogUtils.d(TAG, "【配置回滚】保存失败,预览Bean禁用压缩图"); // 清理临时文件 if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "裁剪失败,清理临时文件:" + _mSourceCropTempFile.getPath()); + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【临时文件清理】裁剪失败,临时文件清理:" + (deleteSuccess ? "成功" : "失败")); } - // 刷新原始预览图 + // 刷新预览,显示最新状态 bvPreviewBackground.reloadPreviewBackground(); } } catch (FileNotFoundException e) { - LogUtils.e(TAG, "文件未找到:" + e.getMessage() + ",保存路径:" + scaledCompressFilePath); - ToastUtils.show("保存失败:文件路径无效"); - // 异常时清理临时文件 + String errMsg = "文件未找到:" + e.getMessage() + ",保存路径:" + scaledCompressFilePath; + LogUtils.e(TAG, "【保存异常-文件未找到】" + errMsg); + ToastUtils.show("保存时发生错误:" + errMsg); + // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } } catch (IOException e) { - LogUtils.e(TAG, "写入异常:" + e.getMessage()); - ToastUtils.show("保存失败:无写入权限或文件损坏"); - // 异常时清理临时文件 + String errMsg = "写入失败:" + e.getMessage() + "(可能是权限不足)"; + LogUtils.e(TAG, "【保存异常-IO错误】" + errMsg); + ToastUtils.show("保存时发生错误:" + errMsg); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + } catch (Exception e) { // 捕获所有异常,避免崩溃和遗漏 + String errMsg = "未知错误:" + e.getMessage(); + LogUtils.e(TAG, "【保存异常-未知错误】" + errMsg, e); + ToastUtils.show("保存时发生错误:" + errMsg); + // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } } finally { - // 关闭流(避免资源泄漏) + // 关闭流资源(避免资源泄漏) if (fos != null) { try { fos.close(); + LogUtils.d(TAG, "【资源清理】文件输出流已关闭"); } catch (IOException e) { - LogUtils.e(TAG, "流关闭异常" + e); - ToastUtils.show("流关闭异常:" + e.getMessage()); + LogUtils.e(TAG, "【资源清理异常】流关闭失败:" + e.getMessage()); } } - // 回收Bitmap(避免内存泄漏) + // 回收Bitmap(避免内存泄漏,尤其是大图片) if (scaledBitmap != null && !scaledBitmap.isRecycled()) { scaledBitmap.recycle(); + LogUtils.d(TAG, "【资源清理】缩放后的Bitmap已回收"); } + LogUtils.d(TAG, "【保存流程】saveCropBitmap 流程结束"); } } - /** - * 缩放Bitmap - */ - private Bitmap scaleBitmap(Bitmap original, float scale) { - if (original == null || original.isRecycled()) { - return null; - } - int width = (int) (original.getWidth() * scale); - int height = (int) (original.getHeight() * scale); - // 确保宽高为正(避免缩放异常) - width = Math.max(width, 1); - height = Math.max(height, 1); - return Bitmap.createScaledBitmap(original, width, height, true); - } + /** + * 缩放Bitmap(按比例缩小,避免OOM) + */ + private Bitmap scaleBitmap(Bitmap original, float scale) { + LogUtils.d(TAG, "【Bitmap缩放】scaleBitmap 触发,缩放比例:" + scale); + if (original == null || original.isRecycled()) { + LogUtils.e(TAG, "【Bitmap缩放失败】原始Bitmap为空或已回收"); + return null; + } + int width = (int) (original.getWidth() * scale); + int height = (int) (original.getHeight() * scale); + // 确保宽高为正(避免缩放比例过小导致宽高为0) + width = Math.max(width, 1); + height = Math.max(height, 1); + LogUtils.d(TAG, "【Bitmap缩放】原始宽高:" + original.getWidth() + "x" + original.getHeight() + ",缩放后宽高:" + width + "x" + height); + return Bitmap.createScaledBitmap(original, width, height, true); // true:开启抗锯齿,提升清晰度 + } - /** - * 分享图片 - */ - void sharePicture() { - BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); - String currentBgPath = utils.getCurrentBackgroundFilePath(); - File fRecivedPicture = new File(currentBgPath); - if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { - ToastUtils.show("分享的背景图片不存在"); - return; - } + /** + * 分享图片(适配Android7.0+,多包名兼容) + */ + void sharePicture() { + LogUtils.d(TAG, "【分享启动】sharePicture 触发,开始处理图片分享"); + BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); + String currentBgPath = utils.getCurrentBackgroundFilePath(); + File fRecivedPicture = new File(currentBgPath); + // 校验分享图片有效性 + LogUtils.d(TAG, "【分享校验】分享图片路径:" + currentBgPath + ",是否存在:" + fRecivedPicture.exists() + ",文件大小:" + fRecivedPicture.length() + " bytes"); + if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { + ToastUtils.show("分享的背景图片不存在"); + LogUtils.e(TAG, "【分享失败】分享图片无效,无法分享"); + return; + } - // 适配Android7.0+ 分享Uri - try { + // 适配Android7.0+ 分享Uri(多包名兼容) + try { Uri uri = getUriForFile(this, fRecivedPicture); + LogUtils.d(TAG, "【分享Uri】分享Uri生成成功:" + uri.toString()); Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.setType("image/" + _mszCommonFileType); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(Intent.createChooser(shareIntent, "Share Image")); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予接收方读取权限 + startActivity(Intent.createChooser(shareIntent, "分享图片")); + LogUtils.d(TAG, "【分享启动】分享意图已启动,等待用户选择分享方式"); } catch (Exception e) { - ToastUtils.show(String.format("sharePicture() Exception : %s", e)); - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + String errMsg = "分享异常:" + e.getMessage(); + ToastUtils.show(errMsg.substring(0, 20)); // 截取前20字,避免吐司过长 + LogUtils.d(TAG, errMsg, Thread.currentThread().getStackTrace()); + LogUtils.e(TAG, "【分享失败】分享意图启动失败,FileProvider配置异常"); } - } - + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); + LogUtils.d(TAG, "【回调触发】onActivityResult 触发,requestCode:" + requestCode + ",resultCode:" + resultCode + "(RESULT_OK=" + RESULT_OK + ")"); + + // 处理图片选择回调(REQUEST_SELECT_PICTURE) if (requestCode == REQUEST_SELECT_PICTURE && resultCode == RESULT_OK) { try { Uri selectedImage = data.getData(); if (selectedImage == null) { ToastUtils.show("选择的图片Uri为空"); + LogUtils.e(TAG, "【选图回调失败】选择的图片Uri为空,无法解析"); return; } - LogUtils.d(TAG, "选择图片Uri : " + selectedImage.toString()); + LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString()); - // 核心修复:对ACTION_GET_CONTENT返回的Uri,添加持久化权限(Android 4.4+) + // 核心修复:对ACTION_GET_CONTENT返回的Uri,添加持久化权限(Android 4.4+,避免后续访问无权限) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { getContentResolver().takePersistableUriPermission( - selectedImage, - Intent.FLAG_GRANT_READ_URI_PERMISSION + selectedImage, + Intent.FLAG_GRANT_READ_URI_PERMISSION ); + LogUtils.d(TAG, "【选图权限】已为选择的图片Uri添加持久化读取权限"); } - // 路径解析逻辑保持不变... + // 路径解析逻辑(适配不同Uri格式,避免解析失败) File fSrcImage = null; String filePath = UriUtil.getFilePathFromUri(this, selectedImage); + LogUtils.d(TAG, "【选图解析】Uri解析后的文件路径:" + filePath); if (!TextUtils.isEmpty(filePath)) { fSrcImage = new File(filePath); } else { + // Uri解析失败,通过流复制生成临时文件(兜底方案) fSrcImage = new File(mfPictureDir, "selected_temp.jpg"); if (fSrcImage.exists()) { fSrcImage.delete(); + LogUtils.d(TAG, "【选图解析】旧临时文件已清理,准备生成新临时文件"); } + // 流复制生成临时文件(适配ContentProvider Uri) FileUtils.copyStreamToFile(getContentResolver().openInputStream(selectedImage), fSrcImage); - LogUtils.d(TAG, "Uri解析失败,通过流复制生成临时文件:" + fSrcImage.getPath()); + LogUtils.d(TAG, "【选图解析】Uri解析失败,通过流复制生成临时文件:" + fSrcImage.getPath()); } + // 校验解析后的图片文件有效性 + LogUtils.d(TAG, "【选图校验】解析后图片文件:路径=" + (fSrcImage != null ? fSrcImage.getAbsolutePath() : "null") + ",是否存在=" + (fSrcImage != null && fSrcImage.exists()) + ",文件大小=" + (fSrcImage != null ? fSrcImage.length() + " bytes" : "0")); if (fSrcImage == null || !fSrcImage.exists() || fSrcImage.length() <= 0) { ToastUtils.show("选择的图片文件不存在或损坏"); + LogUtils.e(TAG, "【选图回调失败】解析后的图片文件无效"); return; } + // 同步图片到预览Bean并刷新预览 BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); utils.saveFileToPreviewBean(fSrcImage, selectedImage.toString()); bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【选图完成】预览图片已更新,启动裁剪(固定比例)"); + // 启动固定比例裁剪 startCropImageActivity(false); } catch (Exception e) { - LogUtils.e(TAG, "选择图片异常" + e); - ToastUtils.show("选择图片失败:" + e.getMessage()); + String errMsg = "选择图片异常:" + e.getMessage(); + LogUtils.e(TAG, errMsg, e); + ToastUtils.show("选择图片失败:" + errMsg.substring(0, 20)); + // 异常时清理裁剪临时文件 if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "选择图片异常,清理临时文件:" + _mSourceCropTempFile.getPath()); + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【选图异常清理】裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getPath()); } } - } else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) { - LogUtils.d(TAG, "REQUEST_TAKE_PHOTO"); - // 检查拍照文件是否有效 + } + // 处理拍照回调(REQUEST_TAKE_PHOTO) + else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) { + LogUtils.d(TAG, "【拍照回调】REQUEST_TAKE_PHOTO 回调触发,开始处理拍照结果"); + // 检查拍照文件是否有效(核心校验,避免空文件) + LogUtils.d(TAG, "【拍照校验】拍照文件路径:" + mfTakePhoto.getAbsolutePath() + ",是否存在:" + mfTakePhoto.exists() + ",文件大小:" + mfTakePhoto.length() + " bytes"); if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { ToastUtils.show("拍照文件不存在或损坏"); + LogUtils.e(TAG, "【拍照回调失败】拍照文件无效,可能是相机未正常保存"); // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "拍照文件无效,清理临时文件"); + LogUtils.d(TAG, "【拍照异常清理】拍照文件无效,清理裁剪临时文件"); } return; } + // 获取拍照Bitmap并处理 Bundle extras = data.getExtras(); + LogUtils.d(TAG, "【拍照回调】拍照数据Bundle:" + (extras != null ? "非空" : "为空")); if (extras != null) { Bitmap imageBitmap = (Bitmap) extras.get("data"); + LogUtils.d(TAG, "【拍照回调】获取拍照Bitmap:" + (imageBitmap != null ? "成功" : "失败") + ",是否回收:" + (imageBitmap != null && imageBitmap.isRecycled())); if (imageBitmap != null && !imageBitmap.isRecycled()) { + // 压缩图片并保存 compressQualityToRecivedPicture(imageBitmap); + LogUtils.d(TAG, "【拍照完成】拍照图片压缩完成,刷新预览并启动裁剪"); // 拍照压缩后刷新预览 bvPreviewBackground.reloadPreviewBackground(); - // 启动裁剪 + // 启动固定比例裁剪 startCropImageActivity(false); } else { ToastUtils.show("拍照图片为空"); + LogUtils.e(TAG, "【拍照回调失败】拍照Bitmap为空或已回收"); // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "拍照图片为空,清理临时文件:" + _mSourceCropTempFile.getPath()); + LogUtils.d(TAG, "【拍照异常清理】拍照图片为空,清理裁剪临时文件:" + _mSourceCropTempFile.getPath()); } } } else { ToastUtils.show("拍照数据获取失败"); + LogUtils.e(TAG, "【拍照回调失败】拍照数据Bundle为空,无法获取图片"); // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "拍照数据获取失败,清理临时文件:" + _mSourceCropTempFile.getPath()); + LogUtils.d(TAG, "【拍照异常清理】拍照数据获取失败,清理裁剪临时文件:" + _mSourceCropTempFile.getPath()); } } - } else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) { - LogUtils.d(TAG, "CROP_IMAGE_REQUEST_CODE"); + } + // 处理裁剪回调(REQUEST_CROP_IMAGE) + else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) { + LogUtils.d(TAG, "【裁剪回调】CROP_IMAGE_REQUEST_CODE 回调触发,开始处理裁剪结果"); try { Bitmap cropBitmap = null; - // 核心修复:优先读取裁剪临时文件(放弃data.getParcelableExtra,避免缩略图) + // 核心修复:优先读取裁剪临时文件(放弃data.getParcelableExtra,避免缩略图/空Bitmap) + LogUtils.d(TAG, "【裁剪回调】裁剪临时文件校验:路径=" + _mSourceCropTempFile.getPath() + ",是否存在=" + _mSourceCropTempFile.exists() + ",文件大小=" + _mSourceCropTempFile.length() + " bytes"); if (_mSourceCropTempFile.exists() && _mSourceCropTempFile.length() > 0) { - LogUtils.d(TAG, String.format("_mSourceCropTempFile 信息:路径=%s , 大小=%d bytes", - _mSourceCropTempFile.getPath(), _mSourceCropTempFile.length())); + LogUtils.d(TAG, "【裁剪回调】裁剪临时文件有效,开始解析Bitmap"); // 核心修复:优化Bitmap解析选项(自动适配格式+防止OOM+避免损坏图片解析失败) BitmapFactory.Options options = new BitmapFactory.Options(); - // 第一步:仅获取图片信息,不加载Bitmap(避免OOM) + // 第一步:仅获取图片信息,不加载Bitmap(避免OOM,尤其是大图片) options.inJustDecodeBounds = true; BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】图片信息:格式=" + (options.outMimeType != null ? options.outMimeType : "未知") + ",原始宽高=" + options.outWidth + "x" + options.outHeight); - // 自动适配图片格式(PNG用ARGB_8888,JPEG用RGB_565省内存) + // 自动适配图片格式(PNG用ARGB_8888保留透明,JPEG用RGB_565省内存) String imageMimeType = options.outMimeType; options.inPreferredConfig = (imageMimeType != null && imageMimeType.contains("png")) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + LogUtils.d(TAG, "【Bitmap解析】自动适配配置:" + options.inPreferredConfig); // 自动计算采样率(防止大图片OOM,最大边长限制为2048) int maxImageSize = 2048; int sampleRate = 1; while (options.outWidth / sampleRate > maxImageSize || options.outHeight / sampleRate > maxImageSize) { - sampleRate *= 2; + sampleRate *= 2; // 每次翻倍采样,确保是2的幂(BitmapFactory要求) } options.inSampleSize = sampleRate; + LogUtils.d(TAG, "【Bitmap解析】采样率计算完成:" + sampleRate + ",目标宽高=" + options.outWidth/sampleRate + "x" + options.outHeight/sampleRate); - // 第二步:正式加载Bitmap + // 第二步:正式加载Bitmap(关闭inJustDecodeBounds) options.inJustDecodeBounds = false; cropBitmap = BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】裁剪Bitmap加载:" + (cropBitmap != null ? "成功" : "失败") + ",加载后大小:" + (cropBitmap != null ? cropBitmap.getByteCount()/1024 + "KB" : "0")); } else { ToastUtils.show("剪裁文件为空或损坏"); + LogUtils.e(TAG, "【裁剪回调失败】裁剪临时文件无效,无法解析"); return; } // 检查解析后的Bitmap是否有效 if (cropBitmap != null && !cropBitmap.isRecycled()) { - saveCropBitmap(cropBitmap); // 调用修复后的保存方法 + LogUtils.d(TAG, "【裁剪回调】裁剪Bitmap有效,开始保存"); + saveCropBitmap(cropBitmap); + // 核心修复:保存后再次刷新预览(双重保障,确保裁剪图实时显示) + runOnUiThread(new Runnable() { + @Override + public void run() { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【裁剪回调】主线程二次刷新预览视图,确保显示最新裁剪图"); + } + }); } else { ToastUtils.show("获取剪裁图片失败(Bitmap解析异常)"); + LogUtils.e(TAG, "【裁剪回调失败】裁剪Bitmap解析失败或已回收"); // 清理无效临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "Bitmap解析失败,清理无效临时文件"); + LogUtils.d(TAG, "【裁剪异常清理】Bitmap解析失败,清理无效临时文件"); } } } catch (OutOfMemoryError e) { - LogUtils.e(TAG, "内存溢出" + e); + LogUtils.e(TAG, "【裁剪异常-OOM】内存溢出:" + e.getMessage()); ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片"); // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } } catch (Exception e) { - LogUtils.e(TAG, "剪裁保存异常" + e); - ToastUtils.show("保存失败:" + e.getMessage()); + LogUtils.e(TAG, "【裁剪异常-未知】剪裁保存异常:" + e.getMessage(), e); + ToastUtils.show("保存时发生错误:" + e.getMessage().substring(0, 20)); // 清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); } } finally { - // 核心修复:移除临时文件清理代码(已移至saveCropBitmap成功后清理) - LogUtils.d(TAG, "裁剪流程结束"); + // 核心修复:移除临时文件清理代码(已移至saveCropBitmap成功后清理,避免提前删除导致保存失败) + LogUtils.d(TAG, "【裁剪回调】裁剪流程结束(临时文件清理由saveCropBitmap负责)"); } - } else if (resultCode != RESULT_OK) { - LogUtils.d(TAG, "操作取消或失败,requestCode: " + requestCode); + } + // 处理操作取消/失败(resultCode != RESULT_OK) + else if (resultCode != RESULT_OK) { + LogUtils.d(TAG, "【操作回调】操作取消或失败,requestCode: " + requestCode + ",resultCode: " + resultCode); ToastUtils.show("操作已取消"); - // 操作取消/失败时,强制清理临时文件 + // 操作取消/失败时,强制清理裁剪临时文件(避免占用空间+下次操作异常) if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "操作取消/失败,清理临时文件:" + _mSourceCropTempFile.getPath()); + boolean deleteSuccess = _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "【操作取消清理】操作取消/失败,裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getPath()); } } } - /** - * 检查类型是否为图片 - */ - private boolean isImageType(String type) { - if (TextUtils.isEmpty(type)) { - return false; - } - return type.startsWith("image/") || "image/jpeg".equals(type) || - "image/jpg".equals(type) || "image/png".equals(type) || - "image/webp".equals(type); - } + /** + * 检查类型是否为图片(适配常见图片格式,避免非图片文件误处理) + */ + private boolean isImageType(String type) { + LogUtils.d(TAG, "【类型校验】isImageType 触发,校验类型:" + (type != null ? type : "null")); + if (TextUtils.isEmpty(type)) { + return false; + } + // 适配常见图片格式:image/* 通用,以及具体格式(jpeg/jpg/png/webp) + boolean isImage = type.startsWith("image/") || "image/jpeg".equals(type) || + "image/jpg".equals(type) || "image/png".equals(type) || + "image/webp".equals(type); + LogUtils.d(TAG, "【类型校验】是否为图片类型:" + isImage); + return isImage; + } /** - * 检查并申请存储权限(修复:适配低版本API,移除Android13+依赖) - */ - private boolean checkAndRequestStoragePermission() { + * 检查并申请存储权限(修复:适配低版本API,移除Android13+依赖,多机型兼容) + */ + private boolean checkAndRequestStoragePermission() { + LogUtils.d(TAG, "【权限校验】checkAndRequestStoragePermission 触发,Android版本:" + Build.VERSION.SDK_INT); + // Android 11+(R):使用所有文件访问权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (!Environment.isExternalStorageManager()) { + boolean hasPermission = Environment.isExternalStorageManager(); + LogUtils.d(TAG, "【权限校验】Android11+ 所有文件访问权限:" + (hasPermission ? "已获取" : "未获取")); + if (!hasPermission) { Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); startActivity(intent); + ToastUtils.show("请开启「所有文件访问权限」"); return false; } - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // 核心修复:同时申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE - String[] permissions = new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - }; - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED - || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + } + // Android 6.0+(M):申请读写外部存储权限 + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + boolean hasReadPerm = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + boolean hasWritePerm = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + LogUtils.d(TAG, "【权限校验】Android6.0+ 存储权限:读=" + hasReadPerm + ",写=" + hasWritePerm); + if (!hasReadPerm || !hasWritePerm) { + // 同时申请读写权限(避免只申请一个导致功能异常) + String[] permissions = new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }; ActivityCompat.requestPermissions(this, permissions, STORAGE_PERMISSION_REQUEST); + LogUtils.d(TAG, "【权限申请】已触发存储权限申请,请求码:" + STORAGE_PERMISSION_REQUEST); return false; } } + // Android 6.0以下:权限默认授予 + 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 isGranted = false; - for (int result : grantResults) { + // 校验所有申请的权限是否都通过(读写权限需同时授予) + 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, "【权限回调】存储权限申请被拒绝,无法正常使用图片功能"); } } } - void setBackgroundColor() { - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - BackgroundBean bean = utils.getCurrentBackgroundBean(); - int nPixelColor = bean.getPixelColor(); - RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1); - if (mainLayout != null) { - mainLayout.setBackgroundColor(nPixelColor); - } - } + /** + * 设置页面背景颜色(适配像素拾取功能,实时更新纯色背景) + */ + void setBackgroundColor() { + LogUtils.d(TAG, "【背景设置】setBackgroundColor 触发,更新页面纯色背景"); + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); + BackgroundBean bean = utils.getCurrentBackgroundBean(); + int nPixelColor = bean.getPixelColor(); + LogUtils.d(TAG, "【背景设置】当前像素颜色值:" + nPixelColor + "(0x" + Integer.toHexString(nPixelColor) + ")"); - @Override - protected void onResume() { - super.onResume(); - setBackgroundColor(); - // Resume时刷新预览(避免后台切换后视图异常) - bvPreviewBackground.reloadPreviewBackground(); - } + RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1); + if (mainLayout != null) { + mainLayout.setBackgroundColor(nPixelColor); + LogUtils.d(TAG, "【背景设置】页面背景颜色更新完成"); + } else { + LogUtils.e(TAG, "【背景设置】主布局控件为空,无法更新背景颜色"); + } + } - public void onNetworkBackgroundDialog(View view) { - // 在需要显示对话框的地方(如网络状态监听回调中) - NetworkBackgroundDialog dialog = new NetworkBackgroundDialog(this, new NetworkBackgroundDialog.OnDialogClickListener() { + @Override + protected void onResume() { + super.onResume(); + LogUtils.d(TAG, "【生命周期】onResume 触发,刷新页面状态"); + // 恢复时更新纯色背景(避免后台切换后颜色显示异常) + setBackgroundColor(); + // Resume时刷新预览(避免后台切换后图片显示异常) + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【生命周期】onResume 完成,背景颜色和预览已刷新"); + } + + /** + * 显示网络图片下载对话框(触发网络背景设置) + */ + 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 + ",文件Url:" + szConfirmFileUrl); + // 保存预览资源信息(用于后续预览和裁剪) preViewFilePath = szConfirmFilePath; preViewFileUrl = szConfirmFileUrl; + // 触发图片接收逻辑 onRecivedPictureListener.onRecivedPicture(preViewFilePath, preViewFileUrl); } @Override public void onCancel() { - // 取消逻辑 + LogUtils.d(TAG, "【网络图片】用户取消网络图片下载"); + // 取消逻辑:无需额外操作,关闭对话框即可 } }); - // 可选:修改对话框标题和内容(适配自定义场景) - dialog.setTitle("网络图片下载对话框"); - dialog.setContent("是否下载地址中的图片资源,作为应用背景图片?"); + // 可选:修改对话框标题和内容(适配自定义场景,提升用户体验) + dialog.setTitle("网络图片下载对话框"); + dialog.setContent("是否下载该地址的图片,作为应用背景?"); + LogUtils.d(TAG, "【网络图片】网络图片对话框配置完成,准备显示"); - // 显示对话框 - dialog.show(); + // 显示对话框 + dialog.show(); + LogUtils.d(TAG, "【网络图片】网络图片对话框已显示"); + } - } + /** + * 图片接收监听器(处理网络图片/分享图片的接收逻辑) + */ + interface OnRecivedPictureListener { + void onRecivedPicture(String srcFilePath, String srcFileUrl); + } - 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并刷新预览 + BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); + utils.saveFileToPreviewBean(srcFile, srcFileUrl); + LogUtils.d(TAG, "【图片接收】图片已同步到预览Bean,刷新预览视图"); + // 修复:网络图片下载后刷新预览(确保图片正常显示) + bvPreviewBackground.reloadPreviewBackground(); + // 启动自由裁剪(网络图片适配自由比例调整) + startCropImageActivity(true); + LogUtils.d(TAG, "【图片接收】已启动自由裁剪,等待用户调整图片比例"); + } + }; - OnRecivedPictureListener onRecivedPictureListener = new OnRecivedPictureListener(){ - @Override - public void onRecivedPicture(String srcFilePath, String srcFileUrl) { - if (TextUtils.isEmpty(srcFilePath)) { - ToastUtils.show("网络图片路径为空"); - return; - } - File srcFile = new File(srcFilePath); - if (!srcFile.exists() || srcFile.length() <= 0) { - ToastUtils.show("网络图片文件不存在或损坏"); - return; - } - BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - utils.saveFileToPreviewBean(srcFile, srcFileUrl); - // 修复:网络图片下载后刷新预览 - bvPreviewBackground.reloadPreviewBackground(); - startCropImageActivity(true); // 自由裁剪 - } - }; - - /** - * 重写finish方法,确保所有退出场景都触发确认提示 - */ - @Override - public void finish() { - if (!isCommitSettings) { - YesNoAlertDialog.show(this, "应用背景更改提示:", "是否应用预览图片?", new YesNoAlertDialog.OnDialogResultListener(){ + /** + * 重写finish方法,确保所有退出场景都触发确认提示(避免用户误操作丢失配置) + */ + @Override + public void finish() { + LogUtils.d(TAG, "【生命周期】finish 触发,是否已提交配置:" + isCommitSettings); + // 未提交配置时,显示确认对话框(避免误退出丢失预览配置) + if (!isCommitSettings) { + LogUtils.d(TAG, "【退出确认】未提交配置,显示应用背景确认对话框"); + YesNoAlertDialog.show(this, "应用背景更改提示:", "是否应用预览图片?", new YesNoAlertDialog.OnDialogResultListener(){ @Override public void onNo() { - isCommitSettings = true; + // 用户选择“不应用”:保留原正式背景,仅更新启用状态 + LogUtils.d(TAG, "【退出确认】用户选择:不应用预览图片,保留原背景配置"); + isCommitSettings = true; // 标记为已提交,避免重复弹窗 BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - BackgroundBean bean = utils.getCurrentBackgroundBean(); - // 修复:根据预览路径是否为空,设置是否启用背景图 - bean.setIsUseBackgroundFile(!TextUtils.isEmpty(preViewFilePath)); - utils.saveSettings(); - finish(); + BackgroundBean currentBean = utils.getCurrentBackgroundBean(); + // 修复:根据预览路径是否为空,同步正式背景启用状态(避免残留无效配置) + currentBean.setIsUseBackgroundFile(!TextUtils.isEmpty(preViewFilePath)); + utils.saveSettings(); // 持久化原配置(仅更新启用状态,不修改图片路径) + LogUtils.d(TAG, "【退出配置】原背景配置保存完成,正式背景启用状态:" + currentBean.isUseBackgroundFile()); + finish(); // 执行真正的退出 } @Override public void onYes() { + // 用户选择“应用”:将预览背景深拷贝到正式背景,覆盖原配置 + LogUtils.d(TAG, "【退出确认】用户选择:应用预览图片到正式背景"); BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - utils.commitPreviewSourceToCurrent(); // 提交预览到正式背景 - isCommitSettings = true; - finish(); + utils.commitPreviewSourceToCurrent(); // 核心:提交预览配置(深拷贝,避免数据污染) + isCommitSettings = true; // 标记为已提交 + LogUtils.d(TAG, "【退出配置】预览背景提交完成,正式背景已更新"); + ToastUtils.show("背景图片应用成功"); // 补充用户反馈,明确操作结果 + finish(); // 执行真正的退出 } }); - } else { - super.finish(); - } - } + } else { + // 已提交配置(或用户已选择弹窗选项),直接执行退出,避免循环 + LogUtils.d(TAG, "【生命周期】已提交配置,执行super.finish()正常退出"); + super.finish(); + } + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java index 44e8988b..3657925e 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java @@ -14,14 +14,16 @@ public class BackgroundBean extends BaseBean { public static final String TAG = "BackgroundPictureBean"; - // 核心字段:背景图片文件名(对应BackgroundSource目录下的图片文件) + // 核心字段:背景图片文件名(对应应用私有目录下的图片文件,与BackgroundSettingsActivity的_mSourceCroppedFile匹配) private String backgroundFileName = ""; // 附加字段:图片信息(如Uri、网络地址等,仅作备注,不参与路径生成) private String backgroundFileInfo = ""; // 控制字段:是否启用背景图片(true-显示背景图,false-显示透明背景) private boolean isUseBackgroundFile = false; - // 核心字段:压缩后背景图片文件名(对应BackgroundSource目录下的压缩图片) + // 核心字段:压缩后背景图片文件名(对应应用私有目录下的压缩图片,与saveCropBitmap的压缩图匹配) private String backgroundScaledCompressFileName = ""; + // 新增:压缩后背景图片完整路径(解决仅存文件名导致的路径拼接错误,适配BackgroundSettingsActivity的私有目录) + private String backgroundScaledCompressFilePath = ""; // 控制字段:是否启用压缩背景图(true-加载压缩图,false-加载原图) private boolean isUseScaledCompress = false; // 裁剪比例字段:背景图宽高比(默认1:1,用于固定比例裁剪) @@ -69,6 +71,17 @@ public class BackgroundBean extends BaseBean { this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName; // 防null } + /** + * 新增:压缩图完整路径 Getter/Setter(适配BackgroundSettingsActivity的saveCropBitmap方法) + */ + public String getBackgroundScaledCompressFilePath() { + return backgroundScaledCompressFilePath; + } + + public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) { + this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath; // 防null,避免路径错误 + } + public boolean isUseScaledCompress() { return isUseScaledCompress; } @@ -109,7 +122,7 @@ public class BackgroundBean extends BaseBean { /** * 将Bean数据写入JSON(序列化,保存到文件) - * 确保所有字段都被写入,无遗漏 + * 确保所有字段都被写入,无遗漏(新增backgroundScaledCompressFilePath字段) */ @Override public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { @@ -119,6 +132,7 @@ public class BackgroundBean extends BaseBean { jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo()); jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile()); jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName()); + jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath()); // 新增字段序列化 jsonWriter.name("isUseScaledCompress").value(bean.isUseScaledCompress()); jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth()); jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight()); @@ -127,7 +141,7 @@ public class BackgroundBean extends BaseBean { /** * 从JSON读取数据到Bean(反序列化,从文件加载) - * 确保所有字段都被读取,兼容旧版本(无字段时设默认值) + * 确保所有字段都被读取,兼容旧版本(无字段时设默认值,新增字段兼容) */ @Override public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { @@ -148,6 +162,9 @@ public class BackgroundBean extends BaseBean { case "backgroundScaledCompressFileName": bean.setBackgroundScaledCompressFileName(jsonReader.nextString()); break; + case "backgroundScaledCompressFilePath": + bean.setBackgroundScaledCompressFilePath(jsonReader.nextString()); // 新增字段反序列化 + break; case "isUseScaledCompress": bean.setIsUseScaledCompress(jsonReader.nextBoolean()); break; @@ -168,5 +185,32 @@ public class BackgroundBean extends BaseBean { jsonReader.endObject(); return bean; } -} + // ====================================== 辅助方法(优化BackgroundSettingsActivity调用体验)====================================== + /** + * 重置背景配置(适配“取消背景”功能,避免残留无效数据) + */ + public void resetBackgroundConfig() { + this.backgroundFileName = ""; + this.backgroundScaledCompressFileName = ""; + this.backgroundScaledCompressFilePath = ""; + this.backgroundFileInfo = ""; + this.isUseBackgroundFile = false; + this.isUseScaledCompress = false; + this.backgroundWidth = 100; + this.backgroundHeight = 100; + } + + /** + * 检查背景配置是否有效(适配BackgroundSettingsActivity的预览/保存校验) + * @return true-配置有效(可显示背景图),false-配置无效 + */ + public boolean isBackgroundConfigValid() { + // 启用背景图时,需确保文件名或完整路径非空 + if (!isUseBackgroundFile) { + return false; + } + return !((backgroundFileName.isEmpty() && backgroundScaledCompressFileName.isEmpty()) + || backgroundScaledCompressFilePath.isEmpty()); + } +} diff --git a/powerbell/src/main/res/xml/file_provider.xml b/powerbell/src/main/res/xml/file_provider.xml index b19a45ff..c2f91517 100644 --- a/powerbell/src/main/res/xml/file_provider.xml +++ b/powerbell/src/main/res/xml/file_provider.xml @@ -1,22 +1,29 @@ - - + + - - + + + path="BackgroundPictureUtils/BackgroundSource/" /> - + + + name="app_temp" + path="temp/" /> - + + + + + @@ -25,7 +32,7 @@ path="." /> + path="." />