diff --git a/powerbell/build.properties b/powerbell/build.properties index 8c70ac16..cc4aea80 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 16:39:04 GMT 2025 +#Sun Nov 30 17:13:20 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=4 +buildCount=7 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 a6823a88..eb59c9e8 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 @@ -16,6 +16,7 @@ import android.view.View; import android.widget.RelativeLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog; import cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libappbase.LogUtils; @@ -48,9 +49,12 @@ 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中配置一致) + private static final String FILE_PROVIDER_AUTHORITY = "cc.winboll.studio.powerbell.fileprovider"; + private AToolbar mAToolbar; private File mfBackgroundDir; // 背景图片存储文件夹 - private File mfPictureDir; // 拍照与剪裁临时文件夹 + private File mfPictureDir; // 拍照与剪裁临时文件夹(权限友好) private File mfTakePhoto; // 拍照文件 // 背景视图预览图片的文件名 @@ -59,16 +63,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg BackgroundView bvPreviewBackground; boolean isCommitSettings = false; - // 静态变量 - // 源文件的临时剪裁图片保存名称 + // 静态变量(裁剪临时文件迁移到临时目录,解决权限冲突) private static String _mSourceCropTempFileName = "SourceCropTemp.jpg"; - // 源文件的临时剪裁图片保存文件对象 - private static File _mSourceCropTempFile; - // 源文件的剪裁图片保存名称 + private static File _mSourceCropTempFile; // 存储在mfPictureDir(临时目录) private static String _mSourceCroppedFileName = "SourceCropped.jpg"; - // 源文件的剪裁图片保存文件对象 private static File _mSourceCroppedFile; - // 源文件的剪裁图片保存路径 private static String _mSourceCroppedFilePath; private static String _mszCommonFileType = "jpeg"; private int mnPictureCompress = 100; @@ -101,12 +100,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mfPictureDir.mkdirs(); } - // 初始化文件对象 + // 初始化文件对象(核心修复1:裁剪临时文件迁移到临时目录,避免私有目录权限冲突) mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg"); - _mSourceCropTempFile = new File(mfBackgroundDir, _mSourceCropTempFileName); + _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())); // 初始化工具栏 mAToolbar = (AToolbar) findViewById(R.id.toolbar); @@ -235,8 +235,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } if (checkAndRequestStoragePermission()) { + // 适配Android 7.0+ 拍照Uri Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); + try { + Uri photoUri = getUriForFile(BackgroundSettingsActivity.this, mfTakePhoto); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); + startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); + } catch (Exception e) { + ToastUtils.show(String.format("sharePicture() Exception : %s", e)); + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } } } }; @@ -277,7 +285,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg OutputStream outStream = null; try { BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); - File fRecivedPicture = new File(utils.getPreviewBackgroundScaledCompressFilePath()); + String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); + File fRecivedPicture = new File(scaledCompressFilePath); // 直接使用完整路径,避免拼接错误 + + // 确保父目录存在 + File parentDir = fRecivedPicture.getParentFile(); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + if (!fRecivedPicture.exists()) { fRecivedPicture.createNewFile(); } @@ -303,74 +319,106 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - /** - * 启动图片裁剪活动(核心修复:传递BackgroundView实际宽高比例,替代Bean默认值) - * @param isCropFree 是否自由裁剪 - */ - public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "startCropImageActivity"); - BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); - BackgroundBean bean = utils.getPreviewBackgroundBean(); - bean.setIsUseScaledCompress(true); - utils.saveSettings(); + /** + * 启动图片裁剪活动(修复:FileProvider适配+意图兼容+异常捕获) + * @param isCropFree 是否自由裁剪 + */ + public void startCropImageActivity(boolean isCropFree) { + LogUtils.d(TAG, "startCropImageActivity"); + BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); + BackgroundBean bean = utils.getPreviewBackgroundBean(); + bean.setIsUseScaledCompress(true); + utils.saveSettings(); - File fRecivedPicture = new File(utils.getPreviewBackgroundFilePath()); + File fRecivedPicture = new File(utils.getPreviewBackgroundFilePath()); + if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { + ToastUtils.show("预览图片不存在或损坏"); + return; + } - Uri uri = UriUtil.getUriForFile(this, fRecivedPicture); - LogUtils.d(TAG, "uri : " + uri.toString()); + // 核心修复1:捕获Uri生成异常(避免FileProvider配置错误导致崩溃) + Uri inputUri = null; + Uri cropOutPutUri = null; + try { + // 适配Android7.0+,用FileProvider生成Content Uri(输入/输出) + inputUri = getUriForFile(this, fRecivedPicture); + LogUtils.d(TAG, "裁剪输入Uri : " + inputUri.toString()); + } catch (Exception e) { + LogUtils.e(TAG, "生成裁剪输入Uri失败:" + e.getMessage()); + ToastUtils.show("图片裁剪失败:无法获取图片权限"); + return; + } - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } + // 清理旧临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } - try { - _mSourceCropTempFile.createNewFile(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - ToastUtils.show("剪裁临时文件创建失败"); - return; - } + try { + _mSourceCropTempFile.createNewFile(); + } catch (IOException e) { + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + ToastUtils.show("剪裁临时文件创建失败"); + return; + } - Uri cropOutPutUri = Uri.fromFile(_mSourceCropTempFile); - LogUtils.d(TAG, "mfTempCropPicture : " + _mSourceCropTempFile.getPath()); + // 生成裁剪输出Uri + try { + cropOutPutUri = getUriForFile(this, _mSourceCropTempFile); + LogUtils.d(TAG, "裁剪输出Uri : " + cropOutPutUri.toString()); + } catch (Exception e) { + LogUtils.e(TAG, "生成裁剪输出Uri失败:" + e.getMessage()); + ToastUtils.show("图片裁剪失败:无法创建临时文件"); + return; + } - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(uri, "image/" + _mszCommonFileType); - intent.putExtra("crop", "true"); - intent.putExtra("noFaceDetection", true); + // 核心修复2:裁剪意图兼容(适配不同机型) + Intent intent = new Intent("com.android.camera.action.CROP"); + // 兼容部分机型不支持隐式意图,添加包名过滤(可选,按需添加) + intent.setPackage("com.android.camera"); + intent.setDataAndType(inputUri, "image/" + _mszCommonFileType); + intent.putExtra("crop", "true"); + intent.putExtra("noFaceDetection", true); - // 修复核心:非自由裁剪时,传递bvPreviewBackground控件的实际宽高比例(确保裁剪与控件匹配) - if (!isCropFree) { - // 1. 优先获取BackgroundView的实际宽高(控件已填充父视图,宽高=父容器尺寸) - int viewWidth = bvPreviewBackground.getWidth(); - int viewHeight = bvPreviewBackground.getHeight(); + // 裁剪比例逻辑保持不变... + 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); + } - // 2. 容错处理:若控件未测量完成(宽/高为0),使用屏幕尺寸兜底 - if (viewWidth <= 0 || viewHeight <= 0) { - viewWidth = getResources().getDisplayMetrics().widthPixels; - viewHeight = getResources().getDisplayMetrics().heightPixels; - LogUtils.d(TAG, "控件未测量完成,使用屏幕尺寸作为裁剪比例:" + viewWidth + "x" + viewHeight); - } + int gcd = calculateGCD(viewWidth, viewHeight); + int simplifiedWidth = viewWidth / gcd; + int simplifiedHeight = viewHeight / gcd; - // 3. 计算宽高比的最大公约数,简化比例(避免过大数值导致裁剪工具不兼容,如1080:1920→9:16) - 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); + } - // 4. 传递简化后的宽高比例给裁剪意图(关键:确保裁剪比例与控件完全匹配) - intent.putExtra("aspectX", simplifiedWidth); - intent.putExtra("aspectY", 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.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - intent.putExtra("return-data", true); - 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); - - startActivityForResult(intent, REQUEST_CROP_IMAGE); - } + // 核心修复3:添加意图启动校验(避免启动失败无响应) + try { + startActivityForResult(intent, REQUEST_CROP_IMAGE); + } catch (Exception e) { + LogUtils.e(TAG, "启动裁剪窗口失败:" + e.getMessage()); + ToastUtils.show("无法启动裁剪工具,请安装系统相机"); + // 兼容方案:若系统相机不支持,使用第三方裁剪工具(可选) + Intent chooserIntent = Intent.createChooser(intent, "选择裁剪工具"); + if (chooserIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(chooserIntent, REQUEST_CROP_IMAGE); + } + } + } /** * 工具方法:计算两个数的最大公约数(用于简化宽高比) @@ -386,134 +434,159 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 保存剪裁后的Bitmap(优化版,修复裁剪后加载不到图片问题) + * 工具方法:生成Content Uri(适配Android 7.0+),需在AndroidManifest.xml中配置FileProvider */ - private void saveCropBitmap(Bitmap bitmap) { - if (bitmap == null) { - ToastUtils.show("剪裁图片为空"); - // 修复:临时文件异常时也清理 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "裁剪图片为空,清理临时文件:" + _mSourceCropTempFile.getPath()); - } - return; - } - - // 内存优化:大图片自动缩放(保持原逻辑) - Bitmap scaledBitmap = bitmap; - if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB - float scale = 1.0f; - while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) { - scale -= 0.2f; // 每次缩小20% - if (scale < 0.2f) break; // 最小缩放到20% - scaledBitmap = scaleBitmap(scaledBitmap, scale); - } - if (scaledBitmap != bitmap) { - bitmap.recycle(); // 回收原Bitmap - } - } - - // 优化:创建保存目录(保持原逻辑) - File backgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); - if (!backgroundDir.exists()) { - if (!backgroundDir.mkdirs()) { - ToastUtils.show("无法创建保存目录"); - if (scaledBitmap != bitmap) scaledBitmap.recycle(); - return; - } - } - - // 剪裁的图片的保存地址(保持原逻辑) - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); - String scaledCompressFileName = utils.getPreviewBackgroundScaledCompressFilePath(); - File fScaledCompressBitmapFile = new File(scaledCompressFileName); - - // 优化:检查文件是否可写(保持原逻辑) - if (fScaledCompressBitmapFile.exists() && !fScaledCompressBitmapFile.canWrite()) { - if (!fScaledCompressBitmapFile.delete()) { - ToastUtils.show("无法删除旧文件"); - if (scaledBitmap != bitmap) scaledBitmap.recycle(); - return; - } - } - - FileOutputStream fos = null; - try { - fos = new FileOutputStream(fScaledCompressBitmapFile); - boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos); - fos.flush(); - if (success) { - ToastUtils.show("图片压缩保存成功"); - BackgroundBean previewBean = utils.getPreviewBackgroundBean(); - // 修复1:同步裁剪后路径到预览Bean(关键!确保加载路径匹配) - // 从压缩文件路径中提取文件名,更新到previewBean的backgroundFileName - String cropFileName = fScaledCompressBitmapFile.getName(); - previewBean.setBackgroundFileName(cropFileName); // 重点:更新为裁剪后的压缩文件名 - // 修复2:强制设置isUseBackgroundFile=true(确保BackgroundView加载图片,而非透明背景) - previewBean.setIsUseBackgroundFile(true); - previewBean.setIsUseScaledCompress(true); - utils.saveSettings(); // 持久化保存Bean,确保路径同步 - - // 修复3:裁剪成功后立即清理临时文件(避免残留冲突) - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "裁剪成功,清理临时文件:" + _mSourceCropTempFile.getPath()); - } - - // 修复:裁剪成功后刷新预览视图(保持原逻辑,此时路径已同步) - bvPreviewBackground.reloadPreviewBackground(); - - } else { - ToastUtils.show("图片压缩保存失败"); - BackgroundBean previewBean = utils.getPreviewBackgroundBean(); - previewBean.setIsUseScaledCompress(false); - utils.saveSettings(); - // 修复:保存失败时清理临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "裁剪失败,清理临时文件:" + _mSourceCropTempFile.getPath()); - } - // 修复:保存失败时刷新原始预览图 - bvPreviewBackground.reloadPreviewBackground(); - } - } catch (FileNotFoundException e) { - LogUtils.e(TAG, "文件未找到" + e); - ToastUtils.show("文件未找到:" + e.getMessage()); - // 异常时清理临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - } catch (IOException e) { - LogUtils.e(TAG, "写入异常" + e); - ToastUtils.show("写入异常:" + e.getMessage()); - // 异常时清理临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "流关闭异常" + e); - ToastUtils.show("流关闭异常:" + e.getMessage()); - } - } - if (scaledBitmap != null && !scaledBitmap.isRecycled()) { - scaledBitmap.recycle(); // 回收缩放后的Bitmap,避免内存泄漏 + private Uri getUriForFile(Context context, File file) throws Exception { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + // 与AndroidManifest.xml中配置的FileProvider授权一致 + return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file); + } catch (Exception e) { + LogUtils.e(TAG, "FileProvider生成Uri失败:" + e.getMessage() + ",文件路径:" + file.getPath()); + throw e; // 抛出异常,让上层处理 } + } else { + return Uri.fromFile(file); // 低版本兼容 } } + /** + * 保存剪裁后的Bitmap(彻底修复:路径拼接+权限+解析异常) + */ + private void saveCropBitmap(Bitmap bitmap) { + if (bitmap == null || bitmap.isRecycled()) { + ToastUtils.show("剪裁图片为空或已回收"); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "裁剪图片为空,清理临时文件:" + _mSourceCropTempFile.getPath()); + } + return; + } + + // 内存优化:大图片自动缩放(保持原逻辑) + Bitmap scaledBitmap = bitmap; + if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB + float scale = 1.0f; + while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) { + scale -= 0.2f; // 每次缩小20% + if (scale < 0.2f) break; // 最小缩放到20% + scaledBitmap = scaleBitmap(scaledBitmap, scale); + } + if (scaledBitmap != bitmap) { + bitmap.recycle(); // 回收原Bitmap + } + } + + // 核心修复1:正确获取保存路径(直接使用完整路径,避免重复拼接) + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); + String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); // 完整路径(无嵌套) + File fScaledCompressBitmapFile = new File(scaledCompressFilePath); + + // 确保保存目录存在(避免路径无效) + File parentDir = fScaledCompressBitmapFile.getParentFile(); + if (!parentDir.exists()) { + if (!parentDir.mkdirs()) { + ToastUtils.show("无法创建保存目录:" + parentDir.getAbsolutePath()); + if (scaledBitmap != bitmap) scaledBitmap.recycle(); + return; + } + } + + // 优化:检查文件权限(确保可写) + if (fScaledCompressBitmapFile.exists()) { + if (!fScaledCompressBitmapFile.canWrite()) { + if (!fScaledCompressBitmapFile.delete()) { + ToastUtils.show("无法删除旧文件(权限不足):" + fScaledCompressBitmapFile.getPath()); + if (scaledBitmap != bitmap) scaledBitmap.recycle(); + return; + } + } + } + + FileOutputStream fos = null; + try { + // 修复2:设置文件可写权限(避免写入失败) + fScaledCompressBitmapFile.setWritable(true, false); + fos = new FileOutputStream(fScaledCompressBitmapFile); + // 压缩保存(80%质量,平衡清晰度和大小) + boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos); + fos.flush(); + if (success) { + ToastUtils.show("图片压缩保存成功"); + BackgroundBean previewBean = utils.getPreviewBackgroundBean(); + // 修复3:同步裁剪后文件名到预览Bean(仅传文件名,避免路径污染) + String cropFileName = fScaledCompressBitmapFile.getName(); + previewBean.setBackgroundFileName(cropFileName); + previewBean.setIsUseBackgroundFile(true); // 强制启用背景图 + previewBean.setIsUseScaledCompress(true); + utils.saveSettings(); // 持久化配置 + + // 清理临时文件(双重保障) + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "裁剪成功,清理临时文件:" + _mSourceCropTempFile.getPath()); + } + + // 刷新预览视图(确保裁剪图实时显示) + bvPreviewBackground.reloadPreviewBackground(); + + } else { + ToastUtils.show("图片压缩保存失败(Bitmap压缩异常)"); + BackgroundBean previewBean = utils.getPreviewBackgroundBean(); + previewBean.setIsUseScaledCompress(false); + utils.saveSettings(); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "裁剪失败,清理临时文件:" + _mSourceCropTempFile.getPath()); + } + // 刷新原始预览图 + bvPreviewBackground.reloadPreviewBackground(); + } + } catch (FileNotFoundException e) { + LogUtils.e(TAG, "文件未找到:" + e.getMessage() + ",保存路径:" + fScaledCompressBitmapFile.getPath()); + ToastUtils.show("保存失败:文件路径无效"); + // 异常时清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + } catch (IOException e) { + LogUtils.e(TAG, "写入异常:" + e.getMessage()); + ToastUtils.show("保存失败:无写入权限或文件损坏"); + // 异常时清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + } finally { + // 关闭流(避免资源泄漏) + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "流关闭异常" + e); + ToastUtils.show("流关闭异常:" + e.getMessage()); + } + } + // 回收Bitmap(避免内存泄漏) + if (scaledBitmap != null && !scaledBitmap.isRecycled()) { + scaledBitmap.recycle(); + } + } + } + /** * 缩放Bitmap */ private Bitmap scaleBitmap(Bitmap original, float scale) { - if (original == null) { + 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); } @@ -522,124 +595,195 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg */ void sharePicture() { BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); - File fRecivedPicture = new File(utils.getCurrentBackgroundFilePath()); - Uri uri = UriUtil.getUriForFile(this, fRecivedPicture); - 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")); - } + String currentBgPath = utils.getCurrentBackgroundFilePath(); + File fRecivedPicture = new File(currentBgPath); + if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { + ToastUtils.show("分享的背景图片不存在"); + return; + } + // 适配Android7.0+ 分享Uri + try { + Uri uri = getUriForFile(this, fRecivedPicture); + + 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")); + } catch (Exception e) { + ToastUtils.show(String.format("sharePicture() Exception : %s", e)); + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + } + } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_SELECT_PICTURE && resultCode == RESULT_OK) { try { Uri selectedImage = data.getData(); - LogUtils.d(TAG, "Uri is : " + selectedImage.toString()); - File fSrcImage = new File(UriUtil.getFilePathFromUri(this, selectedImage)); - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); + if (selectedImage == null) { + ToastUtils.show("选择的图片Uri为空"); + return; + } + LogUtils.d(TAG, "选择图片Uri : " + selectedImage.toString()); - // 修复:保存图片到预览Bean后,立即刷新BackgroundView显示预览图 + // 核心修复1:替换路径解析方式,兼容UriUtil解析失败场景 + File fSrcImage = null; + String filePath = UriUtil.getFilePathFromUri(this, selectedImage); + if (!TextUtils.isEmpty(filePath)) { + fSrcImage = new File(filePath); + } else { + // 兼容方案:通过ContentResolver读取图片流,保存为临时文件 + fSrcImage = new File(mfPictureDir, "selected_temp.jpg"); + if (fSrcImage.exists()) { + fSrcImage.delete(); + } + // 复制图片流到临时文件 + FileUtils.copyStreamToFile(getContentResolver().openInputStream(selectedImage), fSrcImage); + LogUtils.d(TAG, "Uri解析失败,通过流复制生成临时文件:" + fSrcImage.getPath()); + } + + // 核心修复2:增强文件有效性校验 + if (fSrcImage == null || !fSrcImage.exists() || fSrcImage.length() <= 0) { + ToastUtils.show("选择的图片文件不存在或损坏"); + return; + } + + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); + // 保存图片到预览Bean并刷新 utils.saveFileToPreviewBean(fSrcImage, selectedImage.toString()); bvPreviewBackground.reloadPreviewBackground(); - // 启动裁剪(保持原逻辑,裁剪比例已修复) + // 启动裁剪(增加异常捕获) startCropImageActivity(false); } catch (Exception e) { LogUtils.e(TAG, "选择图片异常" + e); ToastUtils.show("选择图片失败:" + e.getMessage()); - // 异常时清理临时文件(避免残留) + // 异常时清理临时文件 if (_mSourceCropTempFile.exists()) { _mSourceCropTempFile.delete(); LogUtils.d(TAG, "选择图片异常,清理临时文件:" + _mSourceCropTempFile.getPath()); } } } else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) { - LogUtils.d(TAG, "REQUEST_TAKE_PHOTO"); - Bundle extras = data.getExtras(); - if (extras != null) { - Bitmap imageBitmap = (Bitmap) extras.get("data"); - if (imageBitmap != null) { - compressQualityToRecivedPicture(imageBitmap); - // 修复:拍照压缩后,刷新预览图 - bvPreviewBackground.reloadPreviewBackground(); - startCropImageActivity(false); - } else { - ToastUtils.show("拍照图片为空"); - // 图片为空时清理临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "拍照图片为空,清理临时文件:" + _mSourceCropTempFile.getPath()); - } - } - } else { - ToastUtils.show("拍照数据获取失败"); - // 数据获取失败时清理临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "拍照数据获取失败,清理临时文件:" + _mSourceCropTempFile.getPath()); - } - } - } else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) { - LogUtils.d(TAG, "CROP_IMAGE_REQUEST_CODE"); - try { - Bitmap cropBitmap = null; - // 方案1:通过Intent获取剪裁后的Bitmap - if (data != null && data.hasExtra("data")) { - cropBitmap = data.getParcelableExtra("data"); - } else if (_mSourceCropTempFile.exists()) { - LogUtils.d(TAG, String.format("_mSourceCropTempFile Exists, Path is :%s ", _mSourceCropTempFile.getPath())); - cropBitmap = BitmapFactory.decodeFile(_mSourceCropTempFile.getPath()); - } else { - ToastUtils.show("剪裁文件不存在"); - return; - } + LogUtils.d(TAG, "REQUEST_TAKE_PHOTO"); + // 检查拍照文件是否有效 + if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { + ToastUtils.show("拍照文件不存在或损坏"); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "拍照文件无效,清理临时文件"); + } + return; + } - if (cropBitmap != null) { - saveCropBitmap(cropBitmap); // 调用保存方法(内含预览刷新+临时文件清理) - } else { - ToastUtils.show("获取剪裁图片失败"); - } - } catch (OutOfMemoryError e) { - LogUtils.e(TAG, "内存溢出" + e); - ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片"); - } catch (Exception e) { - LogUtils.e(TAG, "剪裁保存异常" + e); - ToastUtils.show("保存失败:" + e.getMessage()); - } finally { - // 修复核心:裁剪流程结束后,强制清理临时文件(双重保障,避免残留冲突) - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "裁剪流程结束,强制清理临时文件:" + _mSourceCropTempFile.getPath()); - } - } - } else if (resultCode != RESULT_OK) { - LogUtils.d(TAG, "操作取消或失败,requestCode: " + requestCode); - ToastUtils.show("操作已取消"); - // 修复:操作取消/失败时,强制清理临时文件(避免影响下次裁剪) - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "操作取消/失败,清理临时文件:" + _mSourceCropTempFile.getPath()); - } - } - } + Bundle extras = data.getExtras(); + if (extras != null) { + Bitmap imageBitmap = (Bitmap) extras.get("data"); + if (imageBitmap != null && !imageBitmap.isRecycled()) { + compressQualityToRecivedPicture(imageBitmap); + // 拍照压缩后刷新预览 + bvPreviewBackground.reloadPreviewBackground(); + // 启动裁剪 + startCropImageActivity(false); + } else { + ToastUtils.show("拍照图片为空"); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "拍照图片为空,清理临时文件:" + _mSourceCropTempFile.getPath()); + } + } + } else { + ToastUtils.show("拍照数据获取失败"); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "拍照数据获取失败,清理临时文件:" + _mSourceCropTempFile.getPath()); + } + } + } else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) { + LogUtils.d(TAG, "CROP_IMAGE_REQUEST_CODE"); + try { + Bitmap cropBitmap = null; + // 核心修复:优先读取裁剪临时文件(放弃data.getParcelableExtra,避免缩略图) + if (_mSourceCropTempFile.exists() && _mSourceCropTempFile.length() > 0) { + LogUtils.d(TAG, String.format("_mSourceCropTempFile 信息:路径=%s , 大小=%d bytes", + _mSourceCropTempFile.getPath(), _mSourceCropTempFile.length())); + // 优化Bitmap解析选项(避免OOM和损坏图片解析失败) + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.RGB_565; // 省内存(仅RGB,无透明通道) + options.inSampleSize = 1; // 不缩放(保证清晰度) + options.inJustDecodeBounds = false; + cropBitmap = BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); + } else { + ToastUtils.show("剪裁文件为空或损坏"); + return; + } + + // 检查解析后的Bitmap是否有效 + if (cropBitmap != null && !cropBitmap.isRecycled()) { + saveCropBitmap(cropBitmap); // 调用保存方法(内含修复逻辑) + } else { + ToastUtils.show("获取剪裁图片失败(Bitmap解析异常)"); + // 清理无效临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "Bitmap解析失败,清理无效临时文件"); + } + } + } catch (OutOfMemoryError e) { + LogUtils.e(TAG, "内存溢出" + e); + ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片"); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + } catch (Exception e) { + LogUtils.e(TAG, "剪裁保存异常" + e); + ToastUtils.show("保存失败:" + e.getMessage()); + // 清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + } + } finally { + // 裁剪流程结束,强制清理临时文件(双重保障,避免残留) + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "裁剪流程结束,强制清理临时文件:" + _mSourceCropTempFile.getPath()); + } + } + } else if (resultCode != RESULT_OK) { + LogUtils.d(TAG, "操作取消或失败,requestCode: " + requestCode); + ToastUtils.show("操作已取消"); + // 操作取消/失败时,强制清理临时文件 + if (_mSourceCropTempFile.exists()) { + _mSourceCropTempFile.delete(); + LogUtils.d(TAG, "操作取消/失败,清理临时文件:" + _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); } - /** - * 检查并申请存储权限 + /** + * 检查并申请存储权限(修复:适配低版本API,移除Android13+依赖) */ private boolean checkAndRequestStoragePermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // 仅保留Android 6.0+(API 23)的WRITE_EXTERNAL_STORAGE权限,兼容低版本 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // M = API 23,Android 6.0 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, @@ -654,10 +798,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == STORAGE_PERMISSION_REQUEST) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + boolean isGranted = false; + // 检查权限是否授予 + for (int result : grantResults) { + if (result == PackageManager.PERMISSION_GRANTED) { + isGranted = true; + break; + } + } + if (isGranted) { ToastUtils.show("存储权限已获取"); } else { - ToastUtils.show("需要存储权限才能保存图片"); + ToastUtils.show("需要存储权限才能保存/选择图片"); } } } @@ -667,13 +819,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg BackgroundBean bean = utils.getCurrentBackgroundBean(); int nPixelColor = bean.getPixelColor(); RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1); - mainLayout.setBackgroundColor(nPixelColor); + if (mainLayout != null) { + mainLayout.setBackgroundColor(nPixelColor); + } } @Override protected void onResume() { super.onResume(); setBackgroundColor(); + // Resume时刷新预览(避免后台切换后视图异常) + bvPreviewBackground.reloadPreviewBackground(); } public void onNetworkBackgroundDialog(View view) { @@ -709,16 +865,25 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg 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(new File(srcFilePath), srcFileUrl); + utils.saveFileToPreviewBean(srcFile, srcFileUrl); // 修复:网络图片下载后刷新预览 bvPreviewBackground.reloadPreviewBackground(); - startCropImageActivity(true); + startCropImageActivity(true); // 自由裁剪 } }; /** - * 重写finish方法,确保所有退出场景都触发Toast + * 重写finish方法,确保所有退出场景都触发确认提示 */ @Override public void finish() { @@ -730,7 +895,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg isCommitSettings = true; BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); BackgroundBean bean = utils.getCurrentBackgroundBean(); - bean.setIsUseBackgroundFile(!preViewFilePath.equals("")); + // 修复:根据预览路径是否为空,设置是否启用背景图 + bean.setIsUseBackgroundFile(!TextUtils.isEmpty(preViewFilePath)); utils.saveSettings(); finish(); } @@ -738,7 +904,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onYes() { BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - utils.commitPreviewSourceToCurrent(); + utils.commitPreviewSourceToCurrent(); // 提交预览到正式背景 isCommitSettings = true; finish(); } @@ -748,4 +914,3 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } } - diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java index a2669b93..1d28e620 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java @@ -232,5 +232,28 @@ public class FileUtils { return String.format("%s_%d%s", uniqueId, timeStamp, suffix); } } + + /** + * 复制输入流到文件(兼容Uri解析失败场景) + */ + public static void copyStreamToFile(InputStream inputStream, File file) throws IOException { + if (inputStream == null || file == null) { + return; + } + // 确保父目录存在 + File parentDir = file.getParentFile(); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + FileOutputStream fos = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + fos.flush(); + fos.close(); + inputStream.close(); + } } diff --git a/powerbell/src/main/res/xml/file_provider.xml b/powerbell/src/main/res/xml/file_provider.xml index b20e01b8..b19a45ff 100644 --- a/powerbell/src/main/res/xml/file_provider.xml +++ b/powerbell/src/main/res/xml/file_provider.xml @@ -1,28 +1,36 @@ + + + + + + + + + + + - - - - + - - +