diff --git a/powerbell/build.properties b/powerbell/build.properties index f3bcefd2..623ebe73 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sun Nov 30 23:19:16 GMT 2025 +#Mon Dec 01 00:19:18 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=33 +buildCount=36 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 8ef1e23b..f63f4221 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -2,16 +2,20 @@ 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.content.pm.ResolveInfo; +import android.content.ComponentName; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Matrix; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; +import android.os.Looper; import android.provider.MediaStore; import android.provider.Settings; import android.text.TextUtils; @@ -37,52 +41,36 @@ import cc.winboll.studio.powerbell.utils.UriUtil; import cc.winboll.studio.powerbell.views.BackgroundView; import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.List; -import android.content.pm.ResolveInfo; -import android.content.ComponentName; -import android.os.Looper; -import android.os.Handler; -import android.graphics.Matrix; -import java.io.FileInputStream; public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { public static final String TAG = "BackgroundSettingsActivity"; - public static final int Build_VERSION_CODES_TIRAMISU = 33; - public BackgroundSourceUtils mBackgroundSourceUtils; + public static final int Build_VERSION_CODES_TIRAMISU = 33; + // 工具类单例(唯一文件管理入口) + private BackgroundSourceUtils mBgSourceUtils; - // 图片选择请求码 + // 图片选择/裁剪请求码 public static final int REQUEST_SELECT_PICTURE = 0; public static final int REQUEST_TAKE_PHOTO = 1; public static final int REQUEST_CROP_IMAGE = 2; private static final int STORAGE_PERMISSION_REQUEST = 100; - // FileProvider 授权(必须与AndroidManifest.xml中配置一致,适配多包名) - private static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; - + // 控件 private AToolbar mAToolbar; - private File mfBackgroundDir; // 背景图片存储文件夹 - private File mfPictureDir; // 拍照临时文件夹(权限友好) - private File mfTakePhoto; // 拍照文件 - - // 背景视图预览图片的文件名 + private BackgroundView bvPreviewBackground; + // 拍照临时文件(仅拍照用,路径由工具类间接管理) + private File mfTakePhoto; + // 配置标记 + boolean isCommitSettings = false; + // 预览图片信息(用于退出确认) private String preViewFilePath = ""; private String preViewFileUrl = ""; - BackgroundView bvPreviewBackground; - boolean isCommitSettings = false; - - // 静态变量(核心修改:临时裁剪文件迁移到应用缓存目录) - private static String _mSourceCropTempFileName = "SourceCropTemp.jpg"; - private static File _mSourceCropTempFile; // 存储在【应用缓存目录】(getCacheDir()) - private static String _mSourceCroppedFileName = "SourceCropped.jpg"; - private static File _mSourceCroppedFile; - private static String _mSourceCroppedFilePath; - private static String _mszCommonFileType = "jpeg"; - private int mnPictureCompress = 100; @Override public Activity getActivity() { @@ -95,224 +83,91 @@ 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); + // 初始化工具类(文件管理全依赖此类) + mBgSourceUtils = BackgroundSourceUtils.getInstance(this); - // 1. 初始化工具类和基础文件夹(Java7兼容:显式非空校验) - mBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this); - mfBackgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); - if (!mfBackgroundDir.exists()) { - mfBackgroundDir.mkdirs(); - setDirPermissionsRecursively(mfBackgroundDir); - } + // 初始化拍照临时文件(路径复用App临时目录,权限由工具类保障) + File tempDir = new File(App.getTempDirPath()); + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + mfTakePhoto = new File(tempDir, "TakePhoto.jpg"); - mfPictureDir = new File(App.getTempDirPath()); - if (!mfPictureDir.exists()) { - mfPictureDir.mkdirs(); - setDirPermissionsRecursively(mfPictureDir); - } + // 初始化工具栏 + initToolbar(); + // 初始化按钮点击事件(仅UI交互,无文件逻辑) + initClickListeners(); + // 初始化预览 + initPreview(); + // 处理分享图片意图 + handleShareIntent(); - // 【核心修改1:放弃应用内目录,改用应用私有外部目录(适配MIUI裁剪写入)】 - // 替换原 appInternalDir = getFilesDir(),改为应用私有外部目录(Pictures子目录,MIUI可写) - File appPrivateExternalDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); - if (appPrivateExternalDir == null) { - // 极端兜底:应用私有外部目录不可用,切换到应用缓存目录(getCacheDir()) - appPrivateExternalDir = getCacheDir(); - LogUtils.w(TAG, "【初始化警告】应用私有外部目录不可用,切换到应用缓存目录"); - } - mfBackgroundDir = appPrivateExternalDir; // 同步背景文件目录到应用私有外部目录(避免路径冲突) + LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(精简版,文件管理依赖BackgroundSourceUtils)"); + } - // 【核心修改2:裁剪后文件存储到应用私有外部目录】 - _mSourceCroppedFile = new File(mfBackgroundDir, _mSourceCroppedFileName); - _mSourceCroppedFilePath = _mSourceCroppedFile.getAbsolutePath().toString(); - - // 3. 保留原有变量初始化(Java7兼容) - _mSourceCropTempFileName = "SourceCropTemp.jpg"; - _mSourceCroppedFileName = "SourceCropped.jpg"; - _mszCommonFileType = "jpeg"; - mnPictureCompress = 100; - - // 【核心修改3:裁剪临时文件强制初始化到应用私有外部目录(CropTemp子目录)】 - File cropBaseDir = appPrivateExternalDir; // 固定为应用私有外部目录(MIUI裁剪工具可写) - File cropTempDir = new File(cropBaseDir, "CropTemp"); - if (!cropTempDir.exists()) { - cropTempDir.mkdirs(); - setDirPermissionsRecursively(cropTempDir); // 递归设置权限(确保MIUI裁剪工具可读写) - String dirLog = "【裁剪目录初始化】创建应用私有外部裁剪目录:" + cropTempDir.getAbsolutePath() + - ",目录权限:可写=" + cropTempDir.canWrite() + ",可读=" + cropTempDir.canRead(); - LogUtils.d(TAG, dirLog); - } - - // 初始化裁剪临时文件(先删后建,确保应用私有外部目录权限生效) - _mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName); - try { - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - String deleteLog = "【裁剪文件初始化】删除旧临时文件:" + (deleteSuccess ? "成功" : "失败") + - ",路径:" + _mSourceCropTempFile.getAbsolutePath(); - LogUtils.d(TAG, deleteLog); - } - // 应用私有外部目录创建文件(MIUI裁剪工具可写,Java7兼容权限设置) - _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); // 允许所有用户访问(裁剪工具必需) - _mSourceCropTempFile.setWritable(true, false); - _mSourceCropTempFile.setExecutable(false, false); - String initLog = "【裁剪文件初始化】应用私有外部目录创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath(); - LogUtils.d(TAG, initLog); - } catch (IOException e) { - // 兜底1:应用私有外部子目录失败,切换到应用私有外部根目录 - LogUtils.e(TAG, "【裁剪文件初始化】应用私有外部子目录创建失败,切换到根目录:" + e.getMessage(), e); - _mSourceCropTempFile = new File(appPrivateExternalDir, _mSourceCropTempFileName); - try { - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); - _mSourceCropTempFile.setWritable(true, false); - String cacheLog = "【裁剪文件兜底】应用私有外部根目录创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath(); - LogUtils.d(TAG, cacheLog); - } catch (IOException ex) { - // 终极兜底2:切换到应用缓存目录(getCacheDir(),兼容所有机型) - LogUtils.e(TAG, "【裁剪文件兜底】应用私有外部目录创建失败,切换到缓存目录:" + ex.getMessage(), ex); - File cacheDir = getCacheDir(); - _mSourceCropTempFile = new File(cacheDir, _mSourceCropTempFileName); - try { - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); - _mSourceCropTempFile.setWritable(true, false); - LogUtils.d(TAG, "【裁剪文件终极兜底】应用缓存目录创建临时文件成功:" + _mSourceCropTempFile.getAbsolutePath()); - } catch (IOException exx) { - LogUtils.e(TAG, "【裁剪文件初始化失败】所有目录均无法创建临时文件:" + exx.getMessage(), exx); - runOnUiThread(new Runnable() { - @Override - public void run() { - ToastUtils.show("裁剪文件初始化失败,请重启应用重试"); - } - }); - } - } - } - - // 4. 最终权限校验(确保裁剪临时文件可用) - boolean isFileWritable = false; - boolean isFileReadable = false; - String cropTempFilePath = "null"; - if (_mSourceCropTempFile != null) { - isFileWritable = _mSourceCropTempFile.canWrite(); - isFileReadable = _mSourceCropTempFile.canRead(); - cropTempFilePath = _mSourceCropTempFile.getAbsolutePath(); - } - String dirTypeDesc = getDirTypeDesc(cropBaseDir); - String finalCheckLog = "【初始化】裁剪临时文件最终状态:路径=" + cropTempFilePath + - ",可写=" + isFileWritable + ",可读=" + isFileReadable + - ",目录类型=" + dirTypeDesc; - LogUtils.d(TAG, finalCheckLog); - - // 异常提示(临时文件不可写时提示) - if (!isFileWritable) { - runOnUiThread(new Runnable() { - @Override - public void run() { - ToastUtils.show("裁剪目录权限不足,请重启应用重试"); - } - }); - LogUtils.e(TAG, "【初始化警告】裁剪临时文件不可写,可能导致裁剪失败"); - } - - // 5. 初始化拍照文件(Java7兼容:显式非空校验) - mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg"); - if (mfTakePhoto != null && mfPictureDir.canWrite()) { - try { - if (!mfTakePhoto.exists()) { - mfTakePhoto.createNewFile(); - mfTakePhoto.setReadable(true, false); - mfTakePhoto.setWritable(true, false); - } - } catch (IOException e) { - LogUtils.e(TAG, "【拍照文件初始化】创建失败:" + e.getMessage(), e); - } - } - - // 6. 初始化调试日志(Java7原生字符串拼接) - String mfBackgroundLog = "【初始化】mfBackgroundDir 状态:路径=" + mfBackgroundDir.getAbsolutePath() + - ",是否存在=" + mfBackgroundDir.exists() + ",可写=" + mfBackgroundDir.canWrite(); - LogUtils.d(TAG, mfBackgroundLog); - - String mfPictureLog = "【初始化】mfPictureDir 状态:路径=" + mfPictureDir.getAbsolutePath() + - ",是否存在=" + mfPictureDir.exists() + ",可写=" + mfPictureDir.canWrite(); - LogUtils.d(TAG, mfPictureLog); - - String croppedFileLog = "【初始化】_mSourceCroppedFile 状态:路径=" + _mSourceCroppedFilePath; - if (_mSourceCroppedFile != null && _mSourceCroppedFile.getParentFile() != null) { - croppedFileLog += ",父目录可写=" + _mSourceCroppedFile.getParentFile().canWrite(); - } - LogUtils.d(TAG, croppedFileLog); - - String takePhotoLog = "【初始化】拍照文件状态:路径="; - if (mfTakePhoto != null) { - takePhotoLog += mfTakePhoto.getAbsolutePath() + ",可写=" + mfTakePhoto.canWrite(); - } else { - takePhotoLog += "null"; - } - LogUtils.d(TAG, takePhotoLog); - - // 7. 初始化工具栏(Java7兼容) - mAToolbar = (AToolbar) findViewById(R.id.toolbar); - setActionBar(mAToolbar); - mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture); - getActionBar().setDisplayHomeAsUpEnabled(true); - mAToolbar.setNavigationOnClickListener(new View.OnClickListener() { + /** + * 初始化工具栏(仅UI逻辑) + */ + private void initToolbar() { + 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(); } }); + } - // 8. 设置按钮点击事件(Java7兼容) - 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); + /** + * 初始化所有按钮点击事件(仅UI交互,逻辑调用工具类) + */ + private void initClickListeners() { + 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); + } - // 9. 初始预览(Java7兼容) - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - utils.setCurrentSourceToPreview(); - if (bvPreviewBackground != null) { - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【初始化】预览视图已加载,BackgroundView 状态:正常"); - } else { - LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败"); - } + /** + * 初始化预览(调用工具类同步正式背景到预览) + */ + private void initPreview() { + mBgSourceUtils.setCurrentSourceToPreview(); + if (bvPreviewBackground != null) { + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【初始化】预览视图已加载,BackgroundView 状态:正常"); + } else { + LogUtils.e(TAG, "【初始化】bvPreviewBackground 为空,预览加载失败"); + } + } - // 10. 处理分享的图片(Java7兼容) - Intent intent = getIntent(); - if (intent != null) { - 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(); - String shareLog = "【分享处理】收到分享图片意图(action=" + action + ",type=" + type + "),已显示预览对话框"; - LogUtils.d(TAG, shareLog); - } - } - - LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成(应用私有外部目录裁剪版,适配MIUI)"); - } - - public static String getBackgroundFileName() { - return _mSourceCroppedFileName; + /** + * 处理分享图片意图(仅UI逻辑,文件处理调用工具类) + */ + private void handleShareIntent() { + Intent intent = getIntent(); + if (intent != null) { + 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(); + LogUtils.d(TAG, "【分享处理】收到分享图片意图,已显示预览对话框"); + } + } } @Override @@ -322,250 +177,235 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 更新背景图片预览 - * 如果sourceFile参数为空,则加载旧的背景图片资源 + * 更新背景预览(调用工具类,无文件操作) */ public void updateBackgroundView(File sourceFile, String sourceFileInfo) { LogUtils.d(TAG, "【预览更新】updateBackgroundView 触发,sourceFile是否为空:" + (sourceFile == null)); - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); if (sourceFile == null) { - bvPreviewBackground.reloadCurrentBackground(); // 修复:调用正式背景刷新 + bvPreviewBackground.reloadCurrentBackground(); LogUtils.d(TAG, "【预览更新】sourceFile为空,加载正式背景"); } else { - utils.saveFileToPreviewBean(sourceFile, sourceFileInfo); - bvPreviewBackground.reloadPreviewBackground(); // 修复:调用预览背景刷新 - LogUtils.d(TAG, "【预览更新】预览背景更新完成,图片路径:" + sourceFile.getAbsolutePath()); + mBgSourceUtils.saveFileToPreviewBean(sourceFile, sourceFileInfo); + bvPreviewBackground.reloadPreviewBackground(); + LogUtils.d(TAG, "【预览更新】预览背景更新完成"); } } - // 点击事件监听器:取消背景 + // 点击事件:取消背景(仅操作Bean,无文件逻辑) 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(); + BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); bean.setIsUseBackgroundFile(false); - bean.resetBackgroundConfig(); // 调用Bean辅助方法,清空无效配置 - utils.saveSettings(); - bvPreviewBackground.reloadPreviewBackground(); // 修复:刷新预览 + bean.resetBackgroundConfig(); + mBgSourceUtils.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]; + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发选择图片功能"); + if (checkAndRequestStoragePermission()) { + LogUtils.d(TAG, "【选图权限】存储权限已获取,开始查找图片选择意图"); + // 多意图兜底 + Intent[] intents = new Intent[3]; + // 意图1:ACTION_GET_CONTENT(优先) + Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); + getContentIntent.setType("image/*"); + getContentIntent.addCategory(Intent.CATEGORY_OPENABLE); + getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intents[0] = getContentIntent; + // 意图2:ACTION_PICK(兜底) + Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + pickIntent.setType("image/*"); + pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intents[1] = pickIntent; + // 意图3:ACTION_OPEN_DOCUMENT(Android 4.4+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + openDocIntent.setType("image/*"); + openDocIntent.addCategory(Intent.CATEGORY_OPENABLE); + openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + intents[2] = openDocIntent; + } - // 意图1:ACTION_GET_CONTENT(优先) - Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); - getContentIntent.setType("image/*"); - getContentIntent.addCategory(Intent.CATEGORY_OPENABLE); - getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intents[0] = getContentIntent; + // 查找有效意图 + Intent validIntent = null; + for (Intent intent : intents) { + if (intent != null && intent.resolveActivity(getPackageManager()) != null) { + validIntent = intent; + break; + } + } - // 意图2:ACTION_PICK(兜底) - Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - pickIntent.setType("image/*"); - pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intents[1] = pickIntent; - - // 意图3:ACTION_OPEN_DOCUMENT(Android 4.4+) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - openDocIntent.setType("image/*"); - openDocIntent.addCategory(Intent.CATEGORY_OPENABLE); - openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - intents[2] = openDocIntent; - } - - // 遍历意图,找到第一个能响应的 - Intent validIntent = null; - for (Intent intent : intents) { - if (intent != null && intent.resolveActivity(getPackageManager()) != null) { - validIntent = intent; - break; - } - } - - // 创建chooser时添加flags - if (validIntent != null) { - Intent chooser = Intent.createChooser(validIntent, "选择图片"); - // 核心修复:传递持久化权限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() { + if (validIntent != null) { + Intent chooser = Intent.createChooser(validIntent, "选择图片"); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + startActivityForResult(chooser, REQUEST_SELECT_PICTURE); + LogUtils.d(TAG, "【选图意图】找到有效意图,已启动图片选择"); + } else { + LogUtils.d(TAG, "【选图意图】未找到有效图片选择应用,提示用户安装"); + runOnUiThread(new Runnable() { @Override public void run() { ToastUtils.show("未找到相册应用,请安装后重试"); new AlertDialog.Builder(BackgroundSettingsActivity.this) - .setTitle("无图片选择应用") - .setMessage("需要安装相册应用才能选择图片") - .setPositiveButton("确定", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent marketIntent = new Intent(Intent.ACTION_VIEW); - marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); - // 确保市场意图能响应 - if (marketIntent.resolveActivity(getPackageManager()) != null) { - startActivity(marketIntent); - LogUtils.d(TAG, "【应用市场】已启动应用市场,前往安装系统相册"); - } else { - ToastUtils.show("无法打开应用商店"); - LogUtils.e(TAG, "【应用市场】无法启动应用市场,安装相册失败"); - } - } - }) - .setNegativeButton("取消", null) - .show(); + .setTitle("无图片选择应用") + .setMessage("需要安装相册应用才能选择图片") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent marketIntent = new Intent(Intent.ACTION_VIEW); + marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); + if (marketIntent.resolveActivity(getPackageManager()) != null) { + startActivity(marketIntent); + } else { + ToastUtils.show("无法打开应用商店"); + } + } + }) + .setNegativeButton("取消", null) + .show(); } }); - } - } 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("无可用裁剪图片,请先选择/拍照"); - LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片,文件路径:" + fCheck.getAbsolutePath() + ",是否存在:" + fCheck.exists()); + LogUtils.d(TAG, "【选图权限】存储权限未获取,已触发权限申请"); } } }; - // 点击事件监听器:自由裁剪 + // 点击事件:固定比例裁剪(调用工具类获取裁剪路径,无文件逻辑) + private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能"); + // 从工具类获取正式背景文件,校验有效性 + File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath()); + if (targetFile.exists()) { + startCropImageActivity(false); + LogUtils.d(TAG, "【裁剪启动】固定比例裁剪已启动"); + } else { + ToastUtils.show("无可用裁剪图片,请先选择/拍照"); + LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片"); + } + } + }; + + // 点击事件:自由裁剪(调用工具类获取裁剪路径,无文件逻辑) 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("无可用裁剪图片,请先选择/拍照"); - LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片,文件路径:" + fCheck.getAbsolutePath() + ",是否存在:" + fCheck.exists()); - } - } - }; + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发自由裁剪功能"); + File targetFile = new File(mBgSourceUtils.getCurrentBackgroundFilePath()); + if (targetFile.exists()) { + startCropImageActivity(true); + LogUtils.d(TAG, "【裁剪启动】自由裁剪已启动"); + } else { + ToastUtils.show("无可用裁剪图片,请先选择/拍照"); + LogUtils.e(TAG, "【裁剪失败】无可用裁剪图片"); + } + } + }; - // 点击事件监听器:拍照 - private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【按钮点击】触发拍照功能"); - LogUtils.d(TAG, "【拍照准备】mfTakePhoto 初始路径 : " + mfTakePhoto.getPath()); + // 点击事件:拍照(仅权限+相机意图,文件处理调用工具类) + private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发拍照功能"); + // 清理旧拍照文件 + if (mfTakePhoto.exists()) { + boolean deleteSuccess = mfTakePhoto.delete(); + LogUtils.d(TAG, "【拍照准备】旧拍照文件清理:" + (deleteSuccess ? "成功" : "失败")); + } + // 创建新拍照文件 + try { + boolean createSuccess = mfTakePhoto.createNewFile(); + LogUtils.d(TAG, "【拍照准备】新拍照文件创建:" + (createSuccess ? "成功" : "失败")); + if (!createSuccess) { + ToastUtils.show("拍照文件创建失败"); + return; + } + } catch (IOException e) { + LogUtils.e(TAG, "【拍照异常】文件创建抛出异常:" + e.getMessage()); + ToastUtils.show("拍照文件创建失败"); + return; + } - // 清理旧拍照文件 - if (mfTakePhoto.exists()) { - boolean deleteSuccess = mfTakePhoto.delete(); - LogUtils.d(TAG, "【拍照准备】旧拍照文件清理:" + (deleteSuccess ? "成功" : "失败")); - } - // 创建新拍照文件 - try { - 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.getMessage(), Thread.currentThread().getStackTrace()); - ToastUtils.show("拍照文件创建失败:" + e.getMessage().substring(0, 20)); - return; - } + // 检查权限后启动相机 + if (checkAndRequestStoragePermission()) { + LogUtils.d(TAG, "【拍照权限】存储权限已获取,开始生成拍照Uri"); + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + try { + // 调用工具类获取FileProvider Authority(多包名兼容) + Uri photoUri = FileProvider.getUriForFile(BackgroundSettingsActivity.this, + mBgSourceUtils.getFileProviderAuthority(), mfTakePhoto); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); + startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); + LogUtils.d(TAG, "【拍照启动】相机已启动,拍照Uri:" + photoUri.toString()); + } catch (Exception e) { + String errMsg = "拍照启动异常:" + e.getMessage(); + ToastUtils.show(errMsg.substring(0, 20)); + LogUtils.e(TAG, "【拍照失败】相机启动失败"); + } + } else { + LogUtils.d(TAG, "【拍照权限】存储权限未获取,已触发权限申请"); + } + } + }; - // 检查存储权限后启动相机 - if (checkAndRequestStoragePermission()) { - 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) { - 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, "【拍照权限】存储权限未获取,已触发权限申请"); - } - } - }; + // 点击事件:图片接收(暂未实现) + private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + ToastUtils.show("图片接收功能暂未实现"); + LogUtils.d(TAG, "【按钮点击】触发onReceivedPictureClickListener(暂未实现)"); + } + }; - private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - ToastUtils.show("图片接收功能暂未实现"); - LogUtils.d(TAG, "【按钮点击】触发onReceivedPictureClickListener(暂未实现)"); - } - }; + // 点击事件:像素拾取(调用工具类获取图片路径,无文件逻辑) + private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发像素拾取功能"); + // 从工具类获取正式背景路径 + String targetImagePath = mBgSourceUtils.getCurrentBackgroundFilePath(); + File targetFile = new File(targetImagePath); + if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { + ToastUtils.show("无有效图片可拾取像素"); + LogUtils.e(TAG, "【像素拾取失败】目标图片无效"); + return; + } + // 启动像素拾取Activity + Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class); + intent.putExtra("imagePath", targetImagePath); + startActivity(intent); + LogUtils.d(TAG, "【像素拾取启动】已跳转至PixelPickerActivity"); + } + }; - private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【按钮点击】触发像素拾取功能"); - // 核心修改:通过BackgroundSourceUtils获取正式背景路径,替代旧的_mSourceCroppedFile(避免缓存目录文件依赖) - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - String targetImagePath = utils.getCurrentBackgroundFilePath(); - File targetFile = new File(targetImagePath); - // 校验目标图片有效性 - if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { - ToastUtils.show("无有效图片可拾取像素"); - LogUtils.e(TAG, "【像素拾取失败】目标图片无效:路径=" + (targetFile != null ? targetFile.getAbsolutePath() : "null") + ",是否存在:" + (targetFile != null && targetFile.exists())); - return; - } - // 从文件路径启动像素拾取活动 - Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class); - intent.putExtra("imagePath", targetImagePath); - startActivity(intent); - LogUtils.d(TAG, "【像素拾取启动】已跳转至PixelPickerActivity,图片路径:" + targetImagePath); - } - }; + // 点击事件:清空像素颜色(仅操作Bean,无文件逻辑) + private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】触发像素颜色清空功能"); + BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); + int oldColor = bean.getPixelColor(); + bean.setPixelColor(0); + mBgSourceUtils.saveSettings(); + setBackgroundColor(); + ToastUtils.show("像素颜色已清空"); + LogUtils.d(TAG, "【像素清空】操作完成:旧颜色值=" + oldColor + ",新颜色值=0,配置已保存"); + } + }; - 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())); @@ -576,1099 +416,378 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } OutputStream outStream = null; + FileOutputStream fos = null; // 新增:保留FileOutputStream实例引用 try { - BackgroundSourceUtils utils= BackgroundSourceUtils.getInstance(this); - String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); - File fRecivedPicture = new File(scaledCompressFilePath); // 直接使用完整路径,避免拼接错误 + // 从工具类获取预览压缩图路径(统一路径管理) + String scaledCompressFilePath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); + File fRecivedPicture = new File(scaledCompressFilePath); LogUtils.d(TAG, "【压缩配置】目标压缩路径:" + scaledCompressFilePath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); - // 确保父目录存在 + // 确保父目录存在(调用工具类创建目录,避免重复代码) File parentDir = fRecivedPicture.getParentFile(); if (!parentDir.exists()) { - boolean mkdirSuccess = parentDir.mkdirs(); - LogUtils.d(TAG, "【压缩准备】目标目录创建:" + (mkdirSuccess ? "成功" : "失败") + ",目录路径:" + parentDir.getAbsolutePath()); - if (!mkdirSuccess) { - ToastUtils.show("压缩目录创建失败"); - LogUtils.e(TAG, "【压缩失败】目标目录创建失败,无写入权限"); - return; + mBgSourceUtils.copyFile(new File(""), parentDir); // 复用工具类目录创建逻辑 + LogUtils.d(TAG, "【压缩准备】目标目录已通过工具类创建:" + parentDir.getAbsolutePath()); + } + + // 创建目标文件(调用工具类清理旧文件 → 替换为正确的public接口) + if (fRecivedPicture.exists()) { + mBgSourceUtils.clearOldFileByExternal(fRecivedPicture, "旧压缩文件"); // 关键:用新增的public方法 + } + fRecivedPicture.createNewFile(); + + // 压缩并保存 → 关键修改:保留FileOutputStream实例 + fos = new FileOutputStream(fRecivedPicture); // 子类实例,支持getFD() + outStream = new BufferedOutputStream(fos); // 包装为缓冲流提升性能 + boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); + outStream.flush(); // 先强制刷新缓冲区到文件流 + + // 关键修复:通过FileOutputStream子类实例调用getFD().sync(),确保数据写入磁盘 + if (fos != null) { + try { + fos.getFD().sync(); // 仅子类能调用,强制将文件流同步到物理磁盘 + LogUtils.d(TAG, "【压缩保存】已强制同步数据到磁盘,确保文件写入完成"); + } catch (IOException e) { + // 兼容异常:部分设备/Android版本可能不支持sync(),用flush()兜底 + LogUtils.w(TAG, "【压缩保存】getFD().sync()调用失败,已用flush()兜底:" + e.getMessage()); + outStream.flush(); // 双重兜底,确保数据不丢失 } } - // 创建目标文件(若不存在) - if (!fRecivedPicture.exists()) { - 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); - 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,图片损坏或格式不支持"); + LogUtils.e(TAG, "【压缩失败】Bitmap压缩返回false"); } } catch (IOException e) { - LogUtils.d(TAG, "【压缩异常】IO异常:" + e.getMessage(), Thread.currentThread().getStackTrace()); - ToastUtils.show("图片压缩失败:" + e.getMessage().substring(0, 20)); + LogUtils.e(TAG, "【压缩异常】IO异常:" + e.getMessage()); + ToastUtils.show("图片压缩失败"); } finally { - // 关闭流资源 + // 关闭流资源 → 先关缓冲流,再关文件流(规范操作) if (outStream != null) { try { outStream.close(); - LogUtils.d(TAG, "【压缩清理】输出流已关闭"); } catch (IOException e) { - LogUtils.d(TAG, "【压缩异常】流关闭失败:" + e.getMessage(), Thread.currentThread().getStackTrace()); + LogUtils.e(TAG, "【压缩异常】缓冲流关闭失败:" + e.getMessage()); } } - // 回收Bitmap(避免内存泄漏) - if (bitmap != null && !bitmap.isRecycled()) { - bitmap.recycle(); - LogUtils.d(TAG, "【压缩清理】Bitmap已回收"); - } - } - } - - /** - * 启动图片裁剪活动(全程使用应用内目录,适配Java7+Android全版本,解决Permission denied) - * 核心特性: - * 1. 强制使用应用内目录(getFilesDir())存储裁剪临时文件,无需外部存储权限 - * 2. 兼容Java7语法(无Lambda、Stream、方法引用,仅用匿名内部类/增强for循环) - * 3. 强化应用内目录权限校验+预处理,确保裁剪工具可读写 - * 4. 适配MIUI裁剪工具+Android14+,解决0字节文件/无回调问题 - * @param isCropFree 是否自由裁剪(true=自由比例,false=固定比例) - */ - public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree + "(Android版本:" + Build.VERSION.SDK_INT + ",Java7兼容版)"); - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); - BackgroundBean bean = utils.getPreviewBackgroundBean(); - bean.setIsUseScaledCompress(true); - utils.saveSettings(); - LogUtils.d(TAG, "【裁剪配置】预览Bean已设置启用压缩图,配置已保存"); - - // 第一步:校验预览图片有效性(避免无效图片启动裁剪) - File fRecivedPicture = new File(utils.getPreviewBackgroundFilePath()); - // Java7字符串拼接,避免String.join - String previewPicLog = "【裁剪校验】预览图片状态:" + - "路径=" + fRecivedPicture.getAbsolutePath() + - ",是否存在=" + fRecivedPicture.exists() + - ",文件大小=" + fRecivedPicture.length() + " bytes" + - ",可写=" + fRecivedPicture.canWrite() + - ",可读=" + fRecivedPicture.canRead(); - LogUtils.d(TAG, previewPicLog); - if (!fRecivedPicture.exists() || fRecivedPicture.length() <= 0) { - ToastUtils.show("预览图片不存在或损坏"); - LogUtils.e(TAG, "【裁剪失败】预览图片无效,无法启动裁剪"); - return; - } - - // 第二步:生成裁剪输入Uri(捕获异常,避免FileProvider配置错误崩溃) - Uri inputUri = null; - Uri cropOutPutUri = null; - try { - // 适配Android7.0+,用FileProvider生成Content Uri(输入Uri,适配多包名) - inputUri = getUriForFile(this, fRecivedPicture); - LogUtils.d(TAG, "【裁剪Uri】裁剪输入Uri生成成功 : " + inputUri.toString()); - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】生成裁剪输入Uri失败:" + e.getMessage(), e); - ToastUtils.show("图片裁剪失败:无法获取图片权限"); - // 异常时清理应用内目录临时文件,避免残留 - clearCropTempFile(); - return; - } - - // 第三步:裁剪临时文件预处理(应用内目录权限兜底,解决Permission denied) - try { - // 清理旧裁剪临时文件(应用内目录,避免权限残留/文件锁定) - if (_mSourceCropTempFile != null && _mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【裁剪准备】旧裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); - // 若删除失败,强制重新初始化应用内目录临时文件 - if (!deleteSuccess) { - initCropTempFileAgain(); - } - } - - // 核心:应用内目录权限预校验(确保临时文件可写) - if (_mSourceCropTempFile == null) { - LogUtils.d(TAG, "【裁剪准备】裁剪临时文件为空,重新初始化应用内目录文件"); - initCropTempFileAgain(); // 强制使用应用内目录 - } - // 校验应用内目录父目录权限,无权限则重新初始化 - File cropParentDir = _mSourceCropTempFile.getParentFile(); - if (cropParentDir == null || !cropParentDir.canWrite()) { - LogUtils.d(TAG, "【裁剪准备】应用内目录不可写,强制重新初始化"); - initCropTempFileAgain(); // 传默认参数,强制应用内目录 - } - - // 创建新裁剪临时文件(应用内目录,先校验权限再创建) - _mSourceCropTempFile.createNewFile(); - // Java7兼容:setReadable/setWritable第二个参数设为false(允许所有用户访问,裁剪工具可读写) - _mSourceCropTempFile.setReadable(true, false); - _mSourceCropTempFile.setWritable(true, false); - _mSourceCropTempFile.setExecutable(false, false); - // 打印应用内目录临时文件状态 - String tempFileLog = "【裁剪准备】应用内目录新临时文件创建成功:" + - "路径=" + _mSourceCropTempFile.getAbsolutePath() + - ",可写=" + _mSourceCropTempFile.canWrite() + - ",可读=" + _mSourceCropTempFile.canRead() + - ",目录类型=" + getDirTypeDesc(_mSourceCropTempFile.getParentFile()); - LogUtils.d(TAG, tempFileLog); - } catch (IOException e) { - LogUtils.e(TAG, "【裁剪异常】应用内目录临时文件创建失败(Permission denied):" + e.getMessage(), e); - ToastUtils.show("裁剪临时文件创建失败,请重试"); - // 清理并重新初始化应用内目录文件,为下次操作做准备 - clearCropTempFile(); - initCropTempFileAgain(); - return; - } - - // 第四步:构建裁剪意图(适配MIUI裁剪工具+Android14+,Java7兼容写法) - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(inputUri, "image/*"); // 简化图片类型,适配更多裁剪工具 - intent.putExtra("crop", "true"); // 启用裁剪功能(必传) - intent.putExtra("noFaceDetection", true); // 关闭人脸识别,提升兼容性 - intent.putExtra("circleCrop", false); // 关闭圆形裁剪(适配矩形裁剪场景) - - // 第五步:设置裁剪比例(固定/自由裁剪适配,Java7 if-else判断) - if (!isCropFree) { - int viewWidth = bvPreviewBackground.getWidth(); - int viewHeight = bvPreviewBackground.getHeight(); - // 控件未测量完成时,使用屏幕尺寸作为比例(避免比例为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; - intent.putExtra("aspectX", simplifiedWidth); // 裁剪宽比例 - intent.putExtra("aspectY", simplifiedHeight); // 裁剪高比例 - LogUtils.d(TAG, "【裁剪比例】原始比例:" + viewWidth + ":" + viewHeight + ",简化后比例:" + simplifiedWidth + ":" + simplifiedHeight); - } else { - // 自由裁剪:设置默认比例1:1(兼容部分裁剪工具不支持无比例配置) - intent.putExtra("aspectX", 1); - intent.putExtra("aspectY", 1); - LogUtils.d(TAG, "【裁剪比例】自由裁剪,默认比例1:1(可手动调整)"); - } - - // 第六步:设置裁剪输出尺寸(适配MIUI限制+Android14+ 内存优化) - int screenWidth = getResources().getDisplayMetrics().widthPixels; - int screenHeight = getResources().getDisplayMetrics().heightPixels; - int maxOutputSize = 2048; // MIUI裁剪工具兼容最大尺寸(避免0字节文件) - // 适配Android14+ 内存,进一步限制尺寸(用API版本号替代TIRAMISU,兼容Java7+低编译SDK) - if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { - maxOutputSize = 1536; // Android14+ 最大尺寸限制为1536x1536(平衡清晰度和内存) - } - int outputX = Math.min(screenWidth, maxOutputSize); // 限制宽度≤最大尺寸 - int outputY = Math.min(screenHeight, maxOutputSize); // 限制高度≤最大尺寸 - intent.putExtra("outputX", outputX); // 裁剪后宽度(适配MIUI+Android14+) - intent.putExtra("outputY", outputY); // 裁剪后高度(适配MIUI+Android14+) - intent.putExtra("scale", true); // 允许缩放(必传,否则裁剪后图片模糊) - intent.putExtra("scaleUpIfNeeded", true); // 自动缩放(小图填满裁剪区域,避免空白) - // 打印输出尺寸日志 - String outputSizeLog = "【裁剪适配】输出尺寸设置完成:" + - "outputX=" + outputX + - ",outputY=" + outputY + - ",最大限制=" + maxOutputSize + - "(适配MIUI裁剪+Android14+ 内存)"; - LogUtils.d(TAG, outputSizeLog); - - // 第七步:明确输出格式和质量(避免格式不兼容导致写入失败) - intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); // 固定JPEG格式,兼容性最好 - intent.putExtra("quality", 80); // 裁剪输出质量80%(平衡清晰度和文件大小) - - // 第八步:核心配置(禁用返回Bitmap,避免OOM,输出到应用内临时文件) - intent.putExtra("return-data", false); // 禁用返回Bitmap,必传!(Android14+ 大图片必设) - // 注:此处先不设置EXTRA_OUTPUT,后续生成适配MIUI的outputUri后再设置 - - // 第九步:添加权限Flags(Android14+ 及MIUI裁剪工具必需,Java7兼容写法) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予读取权限(读取输入图片) - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 授予写入权限(写入应用内临时文件) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 适配部分机型裁剪后无回调问题 - // Android14+ 新增:添加FLAG_GRANT_PERSISTABLE_URI_PERMISSION,确保权限持久化 - if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } - LogUtils.d(TAG, "【裁剪权限】意图权限Flags已添加(适配Android14+ 及MIUI,Java7兼容)"); - - // 第十步:适配Android14+ 及MIUI裁剪工具兼容性(获取可用裁剪工具,避免启动无效工具) - try { - // queryIntentActivities 兼容Java7,返回List - List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (!resolveInfos.isEmpty()) { - // 选择第一个可用的裁剪工具(优先系统相机裁剪,适配MIUI) - ResolveInfo resolveInfo = resolveInfos.get(0); - String cropPackageName = resolveInfo.activityInfo.packageName; - String cropActivityName = resolveInfo.activityInfo.name; - String cropToolLog = "【裁剪适配】找到可用裁剪工具:" + - "包名=" + cropPackageName + - ",Activity=" + cropActivityName; - LogUtils.d(TAG, cropToolLog); - - // 核心:生成输出Uri时,显式传入裁剪工具包名(授予精准权限,解决MIUI权限问题) - cropOutPutUri = getUriForFile(this, _mSourceCropTempFile, cropPackageName); // 关键:传入cropPackageName - LogUtils.d(TAG, "【裁剪Uri】应用内目录输出Uri(适配MIUI)生成成功 : " + cropOutPutUri.toString()); - - // 核心:显式设置ComponentName后,清空原意图的data(解决MIUI参数冲突,避免写入失败) - Intent cropIntent = new Intent(intent); - cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); - cropIntent.setDataAndType(null, null); // 关键:清空原data,避免MIUI参数解析冲突 - cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); // 重新设置输出Uri(应用内目录,确保生效) - // 重新添加权限(避免设置Component后权限丢失) - cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { - cropIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } - - // 启动裁剪工具(显式指定Activity,避免启动错误工具) - startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); - String startCropLog = "【裁剪启动】裁剪意图已启动(Java7兼容+应用内目录):" + - "请求码=" + REQUEST_CROP_IMAGE + - ",输出Uri=" + cropOutPutUri.toString() + - ",裁剪工具包名=" + cropPackageName; - LogUtils.d(TAG, startCropLog); - } else { - // 无系统裁剪工具,启动第三方裁剪工具选择器(兜底兼容) - cropOutPutUri = getUriForFile(this, _mSourceCropTempFile); // 生成应用内目录通用输出Uri - intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); // 补充输出Uri - Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); - chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { - chooser.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } - if (chooser.resolveActivity(getPackageManager()) != null) { - startActivityForResult(chooser, REQUEST_CROP_IMAGE); - LogUtils.d(TAG, "【裁剪兼容】已启动第三方裁剪工具选择器(适配无系统裁剪工具场景)"); - } else { - LogUtils.e(TAG, "【裁剪失败】无任何裁剪工具可用,建议安装系统相机"); - ToastUtils.show("无可用裁剪工具,请安装系统相机"); - // 清理应用内目录临时文件,避免占用空间 - clearCropTempFile(); - } - } - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】启动裁剪窗口失败:" + e.getMessage(), e); - ToastUtils.show("无法启动裁剪工具,请重试"); - // 异常时清理应用内目录临时文件,为下次操作做准备 - clearCropTempFile(); - } - } - - /** - * 重新初始化裁剪临时文件(强制使用应用私有外部目录,解决MIUI写入失败) - * 适配说明:兼容Java7(无Lambda/Stream,仅用匿名内部类/if-else),全程禁用应用内目录,确保MIUI裁剪工具可写 - * 优先级:应用私有外部目录 → 应用缓存目录(终极兜底) - * @param forceInternalDir 兼容旧参数,实际强制为false(禁用应用内目录) - */ - private void initCropTempFileAgain(boolean... forceInternalDir) { - // 1. 处理可变参数(Java7兼容:显式校验数组非空,强制禁用应用内目录) - boolean force = false; // 核心:禁用应用内目录,优先应用私有外部目录 - if (forceInternalDir != null && forceInternalDir.length > 0) { - force = false; // 覆盖传入值,确保不切换到应用内目录(避免MIUI写入失败) - } - - // 2. 优先使用应用私有外部目录(MIUI可写,Android 10+ 无需额外权限) - File cropBaseDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); - if (cropBaseDir == null) { - // 兜底1:应用私有外部目录不可用,切换到应用缓存目录(getCacheDir()) - cropBaseDir = getCacheDir(); - LogUtils.w(TAG, "【临时文件重构】应用私有外部目录不可用,切换到应用缓存目录"); - } - - LogUtils.d(TAG, "【临时文件重构】强制使用应用私有外部目录初始化裁剪临时文件:" + cropBaseDir.getAbsolutePath()); - - // 3. 创建裁剪临时目录(应用私有外部目录下的CropTemp子目录) - File cropTempDir = new File(cropBaseDir, "CropTemp"); - if (!cropTempDir.exists()) { - cropTempDir.mkdirs(); - setDirPermissionsRecursively(cropTempDir); // 递归设置权限(Java7兼容,确保MIUI可读写) - String dirLog = "【临时文件重构】创建应用私有外部裁剪目录:" + cropTempDir.getAbsolutePath() + - ",目录权限:可写=" + cropTempDir.canWrite() + ",可读=" + cropTempDir.canRead(); - LogUtils.d(TAG, dirLog); - } - - // 4. 重新初始化裁剪临时文件(应用私有外部目录,先删后建,避免文件锁定) - _mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName); - try { - // 先删除旧文件(Java7兼容:显式校验文件存在,避免空指针) - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - String deleteLog = "【临时文件重构】删除旧临时文件:" + (deleteSuccess ? "成功" : "失败"); - LogUtils.d(TAG, deleteLog); - // 兼容部分机型文件锁定:删除失败时标记为退出时删除 - if (!deleteSuccess) { - _mSourceCropTempFile.deleteOnExit(); - LogUtils.d(TAG, "【临时文件重构】旧文件删除失败,标记为退出时自动删除"); - } - } - - // 校验目录权限(确保可写后再创建,避免无效操作) - if (!cropTempDir.canWrite()) { - throw new IOException("应用私有外部裁剪目录无写入权限:" + cropTempDir.getAbsolutePath()); - } - - // 应用私有外部目录创建新文件(Java7兼容:显式权限设置) - _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); // 允许所有用户访问(裁剪工具必需) - _mSourceCropTempFile.setWritable(true, false); - _mSourceCropTempFile.setExecutable(false, false); - - // 打印初始化结果(Java7原生字符串拼接,避免String.join) - String filePath = _mSourceCropTempFile.getAbsolutePath(); - boolean canWrite = _mSourceCropTempFile.canWrite(); - boolean canRead = _mSourceCropTempFile.canRead(); - String dirType = getDirTypeDesc(cropBaseDir); - String initLog = "【临时文件重构】应用私有外部目录裁剪临时文件初始化成功:路径=" + filePath + - ",可写=" + canWrite + ",可读=" + canRead + ",目录类型=" + dirType; - LogUtils.d(TAG, initLog); - - } catch (IOException e) { - // 异常处理1:应用私有外部子目录失败,切换到应用私有外部根目录 - String errLog = "【临时文件重构】应用私有外部子目录初始化失败:" + e.getMessage(); - LogUtils.e(TAG, errLog, e); - - _mSourceCropTempFile = new File(cropBaseDir, _mSourceCropTempFileName); - try { - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); - _mSourceCropTempFile.setWritable(true, false); - - String cacheLog = "【临时文件重构】应用私有外部根目录兜底初始化成功:" + _mSourceCropTempFile.getAbsolutePath(); - LogUtils.d(TAG, cacheLog); - - } catch (IOException ex) { - // 终极兜底2:应用缓存目录(getCacheDir(),兼容所有机型) - LogUtils.e(TAG, "【临时文件重构】应用私有外部目录兜底失败,切换到缓存目录:" + ex.getMessage(), ex); - File cacheDir = getCacheDir(); - _mSourceCropTempFile = new File(cacheDir, _mSourceCropTempFileName); - try { - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - _mSourceCropTempFile.createNewFile(); - _mSourceCropTempFile.setReadable(true, false); - _mSourceCropTempFile.setWritable(true, false); - LogUtils.d(TAG, "【临时文件重构】应用缓存目录终极兜底成功:" + _mSourceCropTempFile.getAbsolutePath()); - } catch (IOException exx) { - // 极端场景:所有目录均失败,提示用户 - LogUtils.e(TAG, "【临时文件重构】所有目录均初始化失败:" + exx.getMessage(), exx); - runOnUiThread(new Runnable() { - @Override - public void run() { - ToastUtils.show("裁剪临时文件初始化失败,请重启应用重试"); - } - }); - } - } - } - } - - /** - * 工具方法:计算两个数的最大公约数(用于简化宽高比) - * @param a 第一个数(宽) - * @param b 第二个数(高) - * @return 最大公约数 - */ - private int calculateGCD(int a, int b) { - if (b == 0) { - return a; - } - return calculateGCD(b, a % b); - } - - /** - * 工具方法:生成Content Uri(适配Android 7.0+,多包名兼容,适配低版本AndroidX,无任何报错) - * 已修复所有问题: - * 1. 解决pathAlias未定义/空指针问题(显式初始化+全程赋值) - * 2. 解决if-else分支无区别问题(别名校验+路径预处理) - * 3. 解决authority未定义问题(提前定义,作用域全覆盖) - * 4. 解决FileProvider.getPathStrategy()无此函数问题(兼容低版本AndroidX) - * 5. 解决FileProvider路径匹配失败问题(配置适配+文件复制兜底) - * @param context 上下文 - * @param file 目标文件 - * @param targetPackageName 目标应用包名(传入裁剪工具包名,显式授予权限,适配MIUI) - * @return 生成的Content Uri - * @throws Exception 生成失败时抛出异常 - */ - private Uri getUriForFile(Context context, File file, String targetPackageName) throws Exception { - // 【修复1:显式初始化所有变量,彻底解决未定义问题】 - String appInternalDirPath = ""; // 应用内目录(getFilesDir()) - String appPrivateDirPath = ""; // 应用私有外部目录(getExternalFilesDir()) - String cacheDirPath = ""; // 应用缓存目录(getCacheDir()) - String backgroundSourceDirPath = ""; // 背景图片目录 - String filePath = ""; - String pathAlias = "未知(未初始化)"; // pathAlias 显式初始化,无未定义 - String pathType = "未知目录"; - - // 初始化目录路径(显式非空校验,Java7兼容,避免空指针) - if (file != null) { - filePath = file.getAbsolutePath(); - } - - File appInternalDir = getFilesDir(); - if (appInternalDir != null) { - appInternalDirPath = appInternalDir.getAbsolutePath(); - } - - File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); - if (appPrivateDir != null) { - appPrivateDirPath = appPrivateDir.getAbsolutePath(); - } - - File cacheDir = getCacheDir(); - if (cacheDir != null) { - cacheDirPath = cacheDir.getAbsolutePath(); - } - - File backgroundSourceDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()); - if (backgroundSourceDir != null) { - backgroundSourceDirPath = backgroundSourceDir.getAbsolutePath(); - } - - // 【修复2:pathAlias 全程赋值,无任何空值场景】 - if (!TextUtils.isEmpty(filePath)) { - if (!TextUtils.isEmpty(appInternalDirPath) && filePath.contains(appInternalDirPath)) { - pathType = "应用内目录(getFilesDir())"; - pathAlias = "app_internal_files"; // 与res/xml/file_paths.xml中files-path的name严格一致 - } else if (!TextUtils.isEmpty(backgroundSourceDirPath) && filePath.contains(backgroundSourceDirPath)) { - pathType = "背景图片目录(BackgroundSource)"; - pathAlias = "background_source"; // 与res/xml/file_paths.xml中external-files-path的name一致 - } else if (!TextUtils.isEmpty(appPrivateDirPath) && filePath.contains(appPrivateDirPath)) { - pathType = "应用私有外部目录(Pictures)"; - pathAlias = "app_private_pictures"; // 与res/xml/file_paths.xml中external-files-path的name一致 - } else if (!TextUtils.isEmpty(cacheDirPath) && filePath.contains(cacheDirPath)) { - pathType = "应用内部缓存目录"; - pathAlias = "cache_path"; // 与res/xml/file_paths.xml中cache-path的name一致 - } else { - pathType = "外部存储目录"; - pathAlias = "未知(未匹配到配置)"; // 明确赋值,无空值 - } - } else { - pathAlias = "未知(文件路径为空)"; // 文件为空时也赋值,彻底规避未定义 - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // 【修复3:提前定义authority,作用域覆盖try/catch,无未定义】 - String authority = BuildConfig.APPLICATION_ID + ".fileprovider"; - try { - Uri uri = null; - - // 【修复4:优化if-else分支逻辑(无getPathStrategy()),真正有区别】 - // 分支1:匹配到有效别名(非“未知”)→ 预处理路径+默认生成(提升匹配成功率) - if (!TextUtils.isEmpty(pathAlias) && !pathAlias.contains("未知")) { - // 关键优化:提前校验文件路径是否在配置的根目录下(避免路径匹配失败) - boolean isPathValid = false; - if (pathAlias.equals("app_internal_files") && !TextUtils.isEmpty(appInternalDirPath)) { - isPathValid = filePath.startsWith(appInternalDirPath); // 应用内目录路径校验 - } else if (pathAlias.equals("background_source") && !TextUtils.isEmpty(backgroundSourceDirPath)) { - isPathValid = filePath.startsWith(backgroundSourceDirPath); // 背景目录路径校验 - } else if (pathAlias.equals("app_private_pictures") && !TextUtils.isEmpty(appPrivateDirPath)) { - isPathValid = filePath.startsWith(appPrivateDirPath); // 应用私有外部目录校验 - } else if (pathAlias.equals("cache_path") && !TextUtils.isEmpty(cacheDirPath)) { - isPathValid = filePath.startsWith(cacheDirPath); // 缓存目录校验 - } - - if (isPathValid) { - // 路径有效,直接生成Uri(兼容低版本AndroidX,无getPathStrategy()) - uri = FileProvider.getUriForFile(context, authority, file); - LogUtils.d(TAG, "【FileProvider】有效别名+路径校验通过,生成Uri(兼容低版本AndroidX):别名=" + pathAlias); - } else { - // 路径无效,提示配置问题(避免盲目生成Uri失败) - throw new IllegalArgumentException("路径与别名不匹配:文件路径=" + filePath + ",别名=" + pathAlias + ",请检查file_paths.xml配置"); - } - } - // 分支2:未匹配到有效别名(含“未知”)→ 默认生成+警告日志(提示配置) - else { - uri = FileProvider.getUriForFile(context, authority, file); - LogUtils.w(TAG, "【FileProvider】未匹配到有效别名(建议检查file_paths.xml),使用默认方式生成Uri:别名=" + pathAlias); - } - - // 显式授予裁剪工具权限(适配MIUI,拒绝通配符“*”,避免权限拒绝) - if (!TextUtils.isEmpty(targetPackageName)) { - context.grantUriPermission( - targetPackageName, - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ); - LogUtils.d(TAG, "【FileProvider】已显式授予裁剪工具[" + targetPackageName + "]读写权限(适配MIUI)"); - } else { - context.grantUriPermission( - "*", - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ); - LogUtils.d(TAG, "【FileProvider】已授予所有应用临时权限(兜底兼容)"); - } - - // 详细日志(便于调试路径匹配问题) - LogUtils.d(TAG, "【FileProvider】Uri生成成功:" + - "包名=" + BuildConfig.APPLICATION_ID + - ",Authority=" + authority + - ",文件路径=" + filePath + - ",路径类型=" + pathType + - ",FileProvider别名=" + pathAlias + - ",目标授权包名=" + (targetPackageName != null ? targetPackageName : "无") + - ",最终Uri=" + uri.toString()); - return uri; - - } catch (IllegalArgumentException e) { - // 【修复5:路径匹配失败兜底(复制文件到应用私有外部目录,必成功)】 - String errMsg = "【FileProvider路径匹配失败】" + - "包名=" + BuildConfig.APPLICATION_ID + - ",Authority=" + authority + - ",文件路径=" + filePath + - ",路径类型=" + pathType + - ",FileProvider别名=" + pathAlias + - ",错误信息:" + e.getMessage(); - LogUtils.e(TAG, errMsg, e); - - // 兜底方案:将文件复制到应用私有外部目录(已配置,适配所有机型) - File fallbackFile = copyToFallbackDir(file); - if (fallbackFile != null) { - LogUtils.d(TAG, "【FileProvider兜底】已复制文件到应用私有外部目录:" + fallbackFile.getAbsolutePath()); - return getUriForFile(context, fallbackFile, targetPackageName); // 递归生成Uri(必成功) - } else { - throw new Exception("FileProvider生成Uri失败(路径匹配+兜底均失败):" + errMsg); - } - - } catch (Exception e) { - // 其他异常(权限/文件损坏等),打印详细日志 - String errMsg = "FileProvider生成Uri失败:" + - "包名=" + BuildConfig.APPLICATION_ID + - ",Authority=" + authority + - ",文件路径=" + filePath + - ",路径类型=" + pathType + - ",FileProvider别名=" + pathAlias + - ",错误信息:" + e.getMessage(); - LogUtils.e(TAG, errMsg, e); - throw new Exception(errMsg); - } - } else { - // Android 7.0以下:直接生成Uri(兼容旧机型,无任何依赖) - Uri uri = Uri.fromFile(file); - LogUtils.d(TAG, "【兼容旧机型】Android7.0以下(API<24),直接生成Uri:" + - "文件路径=" + filePath + - ",Uri=" + uri.toString() + - ",路径类型=" + pathType + - ",FileProvider别名=" + pathAlias); - return uri; - } - } - - /** - * 辅助函数:将文件复制到应用私有外部目录(兜底方案,解决FileProvider路径匹配失败) - * @param sourceFile 源文件(应用内目录的临时文件) - * @return 复制后的兜底文件(应用私有外部目录),复制失败返回null - */ - private File copyToFallbackDir(File sourceFile) { - if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) { - LogUtils.e(TAG, "【FileProvider兜底】源文件无效,无法复制:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); - return null; - } - - // 兜底目录:应用私有外部目录(已配置,适配所有机型,无权限问题) - File fallbackDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "CropFallback"); - if (!fallbackDir.exists()) { - fallbackDir.mkdirs(); - setDirPermissionsRecursively(fallbackDir); // 强制设置权限(确保可写) - LogUtils.d(TAG, "【FileProvider兜底】创建应用私有外部兜底目录:" + fallbackDir.getAbsolutePath()); - } - - // 复制文件(保持原文件名,避免冲突) - File fallbackFile = new File(fallbackDir, sourceFile.getName()); - try { - // 先删除旧的兜底文件(避免文件锁定) - if (fallbackFile.exists()) { - fallbackFile.delete(); - } - // 流复制(适配应用内目录文件读取,显式定义输入流,无空指针) - FileInputStream fis = new FileInputStream(sourceFile); - FileUtils.copyStreamToFile(fis, fallbackFile); - // 强制设置权限(确保裁剪工具可读写) - fallbackFile.setReadable(true, false); - fallbackFile.setWritable(true, false); - LogUtils.d(TAG, "【FileProvider兜底】文件复制成功:" + - "源路径=" + sourceFile.getAbsolutePath() + - ",兜底路径=" + fallbackFile.getAbsolutePath() + - ",文件大小=" + fallbackFile.length() + " bytes"); - return fallbackFile; - } catch (Exception e) { - LogUtils.e(TAG, "【FileProvider兜底】文件复制失败:" + e.getMessage(), e); - return null; - } - } - - // 保留原无参数getUriForFile(兼容其他调用场景,避免编译报错) - private Uri getUriForFile(Context context, File file) throws Exception { - return getUriForFile(context, file, null); // 调用新方法,目标包名传null(兜底) - } - - /** - * 保存剪裁后的Bitmap(终极修复:强化预览同步+双重刷新+权限适配,确保图片加载到bvPreviewBackground控件) - * 核心目标:将裁剪后的图片同步到预览Bean+直接设置到BackgroundView,双重保障显示 - */ - private void saveCropBitmap(Bitmap bitmap) { - LogUtils.d(TAG, "【保存启动】saveCropBitmap 触发,开始处理裁剪图片(临时文件位于应用私有外部目录)"); - if (bitmap == null || bitmap.isRecycled()) { - ToastUtils.show("裁剪图片为空,请重新裁剪"); - LogUtils.e(TAG, "【保存失败】裁剪图片为空或已回收,无法保存"); - // 清理应用私有外部目录下的裁剪临时文件(避免残留空文件) - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【临时文件清理】裁剪图片无效,应用私有外部目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); - } - return; - } - - // 内存优化:大图片自动缩放(超过10MB缩小,避免OOM,适配高清图片) - Bitmap scaledBitmap = bitmap; - 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() / 1024 / 1024 > 5) { // 缩小到5MB以内(平衡清晰度和内存) - scale -= 0.2f; // 每次缩小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,释放内存(避免内存泄漏) - LogUtils.d(TAG, "【内存回收】原始Bitmap已回收"); - } - } - - // 核心1:通过BackgroundSourceUtils获取统一的预览压缩图路径(确保与预览逻辑完全对齐,适配工具类路径管理) - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); - String scaledCompressFilePath = utils.getPreviewBackgroundScaledCompressFilePath(); // 统一路径:由BackgroundSourceUtils管理,避免手动设置冲突 - File fScaledCompressBitmapFile = new File(scaledCompressFilePath); - LogUtils.d(TAG, "【保存准备】通过BackgroundSourceUtils获取统一保存路径:" + scaledCompressFilePath + ",裁剪临时文件路径:" + _mSourceCropTempFile.getAbsolutePath()); - - // 确保保存目录存在(避免路径无效导致保存失败,适配应用私有外部目录) - File parentDir = fScaledCompressBitmapFile.getParentFile(); - LogUtils.d(TAG, "【保存准备】目标保存目录:" + parentDir.getAbsolutePath() + ",是否存在:" + parentDir.exists()); - if (!parentDir.exists()) { - boolean mkdirSuccess = parentDir.mkdirs(); - // 强制设置目录权限(Android14+ 必须,否则保存失败) - parentDir.setWritable(true, false); - parentDir.setReadable(true, false); - LogUtils.d(TAG, "【保存准备】目录不存在,创建结果:" + (mkdirSuccess ? "成功" : "失败") + ",目录权限:可写=" + parentDir.canWrite()); - 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()) { - 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; - } - } - } - - FileOutputStream fos = null; - try { - // 强制设置文件可写权限(Android14+ 应用私有外部目录必须显式设置,否则写入失败) - fScaledCompressBitmapFile.setWritable(true, false); - fScaledCompressBitmapFile.setReadable(true, false); - LogUtils.d(TAG, "【保存准备】目标文件权限设置完成:可写=" + fScaledCompressBitmapFile.canWrite() + ",可读=" + fScaledCompressBitmapFile.canRead()); - fos = new FileOutputStream(fScaledCompressBitmapFile); - - // 自动适配压缩格式(根据工具类路径中的文件名区分JPEG/PNG,避免格式错误导致图片损坏) - String fileName = fScaledCompressBitmapFile.getName(); - Bitmap.CompressFormat compressFormat = fileName.endsWith(".png") - ? Bitmap.CompressFormat.PNG - : Bitmap.CompressFormat.JPEG; - LogUtils.d(TAG, "【保存配置】压缩格式:" + compressFormat + ",压缩质量:80%(平衡清晰度和文件大小)"); - - // 压缩保存(80%质量为最优选择,避免过度压缩导致模糊) - boolean success = scaledBitmap.compress(compressFormat, 80, fos); - fos.flush(); // 强制写入数据,避免数据残留(解决部分机型保存后文件损坏问题) - fos.getFD().sync(); // 同步文件描述符,确保数据完全写入磁盘(Android14+ 关键) - LogUtils.d(TAG, "【保存结果】图片压缩保存:" + (success ? "成功" : "失败") + ",目标路径:" + scaledCompressFilePath + ",文件大小:" + (success ? fScaledCompressBitmapFile.length()/1024 + "KB" : "0")); - - if (success) { - ToastUtils.show("图片保存成功"); - BackgroundBean previewBean = utils.getPreviewBackgroundBean(); - // 核心2:同步预览Bean配置(仅保留与BackgroundSourceUtils兼容的字段,删除手动设置路径的冲突代码) - previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath); // 压缩图路径(由工具类管理,无需额外设置原始路径) - previewBean.setIsUseBackgroundFile(true); // 强制启用背景图,确保预览加载 - previewBean.setIsUseScaledCompress(true); // 启用压缩图,提升预览加载速度 - utils.saveSettings(); // 持久化配置到JSON文件(由工具类统一管理,避免路径混乱) - LogUtils.d(TAG, "【Bean同步】预览Bean配置同步完成(适配BackgroundSourceUtils路径管理):压缩图路径=" + previewBean.getBackgroundScaledCompressFilePath() + ",是否启用背景=" + previewBean.isUseBackgroundFile()); - - // 核心3:调用工具类同步文件(与BackgroundSourceUtils逻辑完全对齐,由工具类统一管理预览目录文件) - utils.saveFileToPreviewBean(fScaledCompressBitmapFile, scaledCompressFilePath); - LogUtils.d(TAG, "【文件同步】已调用saveFileToPreviewBean,由工具类同步裁剪图到预览目录(应用私有外部目录)"); - - // 核心4:直接设置图片路径到bvPreviewBackground(跳过Bean间接同步,确保实时显示) - // 适配BackgroundView:若有setPreviewBackgroundPath方法则直接调用,无则注释此段(需手动在BackgroundView新增) - if (bvPreviewBackground != null) { - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【直接设置】已将裁剪图路径直接设置到bvPreviewBackground:" + scaledCompressFilePath); - } - - // 核心5:主线程双重刷新预览(确保图片加载到bvPreviewBackground,解决刷新不及时问题) - runOnUiThread(new Runnable() { - @Override - public void run() { - // 第一次刷新:立即刷新视图 - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第一次刷新bvPreviewBackground(立即)"); - // 第二次刷新:延迟300ms(适配控件渲染耗时,避免视图未更新) - new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第二次刷新bvPreviewBackground(延迟300ms),确保图片正常显示"); - } - }, 300); - } - }); - - // 保存成功后,清理应用私有外部目录下的裁剪临时文件(释放空间,避免残留) - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【临时文件清理】裁剪成功,应用私有外部目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); - } - - } else { - String errMsg = "图片压缩失败(Bitmap压缩异常)"; - LogUtils.e(TAG, "【保存失败】" + errMsg); - ToastUtils.show("保存时发生错误:" + errMsg); - // 保存失败,回滚配置(避免预览显示异常,适配工具类路径管理) - BackgroundBean previewBean = utils.getPreviewBackgroundBean(); - previewBean.setIsUseScaledCompress(false); - previewBean.setIsUseBackgroundFile(false); - utils.saveSettings(); - LogUtils.d(TAG, "【配置回滚】保存失败,预览Bean禁用背景图(避免预览异常)"); - // 清理应用私有外部目录下的裁剪临时文件 - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【临时文件清理】裁剪失败,应用私有外部目录临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getAbsolutePath()); - } - // 刷新预览,显示最新状态(避免残留旧图) - if (bvPreviewBackground != null) { - bvPreviewBackground.reloadPreviewBackground(); - } - } - } catch (FileNotFoundException e) { - String errMsg = "文件未找到:" + e.getMessage() + ",保存路径:" + scaledCompressFilePath; - LogUtils.e(TAG, "【保存异常-文件未找到】" + errMsg); - ToastUtils.show("保存时发生错误:" + errMsg); - // 清理临时文件 - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - } - } catch (IOException e) { - 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) { + if (fos != null) { // 新增:关闭FileOutputStream try { fos.close(); - LogUtils.d(TAG, "【资源清理】文件输出流已关闭"); } catch (IOException e) { - LogUtils.e(TAG, "【资源清理异常】流关闭失败:" + e.getMessage()); + LogUtils.e(TAG, "【压缩异常】文件流关闭失败:" + e.getMessage()); } } - // 回收缩放后的Bitmap(避免内存泄漏,尤其是高清图片裁剪后) - if (scaledBitmap != null && !scaledBitmap.isRecycled()) { - scaledBitmap.recycle(); - LogUtils.d(TAG, "【资源清理】缩放后的Bitmap已回收(释放内存)"); + // 回收Bitmap + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); } - LogUtils.d(TAG, "【保存流程】saveCropBitmap 流程结束(适配BackgroundSourceUtils路径管理)"); } } - /** - * 辅助方法:缩放Bitmap(适配大图片压缩,避免OOM) - * @param bitmap 原始Bitmap - * @param scale 缩放比例(0.1~1.0) - * @return 缩放后的Bitmap - */ - private Bitmap scaleBitmap(Bitmap bitmap, float scale) { - if (bitmap == null || scale <= 0 || scale >= 1.0f) { - return bitmap; - } - int width = Math.round(bitmap.getWidth() * scale); - int height = Math.round(bitmap.getHeight() * scale); - // 使用Matrix缩放,保证图片清晰度 - Matrix matrix = new Matrix(); - matrix.postScale(scale, scale); - Bitmap scaledBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - LogUtils.d(TAG, "【Bitmap缩放】缩放前:" + bitmap.getWidth() + "x" + bitmap.getHeight() + ",缩放后:" + width + "x" + height); - return scaledBitmap; - } + /** + * 启动图片裁剪活动(核心:调用工具类获取裁剪路径,精简文件管理逻辑) + * @param isCropFree 是否自由裁剪 + */ + public void startCropImageActivity(boolean isCropFree) { + LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setIsUseScaledCompress(true); + mBgSourceUtils.saveSettings(); - /** - * 分享图片(适配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; - } + // 1. 校验预览图片有效性(从工具类获取路径) + File previewFile = new File(mBgSourceUtils.getPreviewBackgroundFilePath()); + LogUtils.d(TAG, "【裁剪校验】预览图片状态:路径=" + previewFile.getAbsolutePath() + ",是否存在=" + previewFile.exists()); + if (!previewFile.exists() || previewFile.length() <= 0) { + ToastUtils.show("预览图片不存在或损坏"); + LogUtils.e(TAG, "【裁剪失败】预览图片无效"); + return; + } - // 适配Android7.0+ 分享Uri(多包名兼容,支持缓存目录/私有目录文件) - try { - Uri uri = getUriForFile(this, fRecivedPicture); - LogUtils.d(TAG, "【分享Uri】分享Uri生成成功:" + uri.toString() + ",文件是否来自缓存目录:" + fRecivedPicture.getAbsolutePath().contains(getCacheDir().getAbsolutePath())); + // 2. 生成裁剪输入Uri(调用工具类的FileProvider Authority) + Uri inputUri = null; + try { + inputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), previewFile); + LogUtils.d(TAG, "【裁剪Uri】输入Uri生成成功 : " + inputUri.toString()); + } catch (Exception e) { + LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败:" + e.getMessage(), e); + ToastUtils.show("图片裁剪失败:无法获取图片权限"); + mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理临时文件 + return; + } - 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); - // 兼容部分机型分享无响应,添加类别和flags - shareIntent.addCategory(Intent.CATEGORY_DEFAULT); - shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 3. 调用工具类创建裁剪路径(系统裁剪可读写) + File cropTempFile = mBgSourceUtils.createCropFileProviderPath(); + if (cropTempFile == null) { + ToastUtils.show("裁剪路径创建失败,请重试"); + return; + } - // 启动分享选择器,避免直接启动无响应 - Intent chooser = Intent.createChooser(shareIntent, "选择分享方式"); - chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 传递权限给选择器 - if (chooser.resolveActivity(getPackageManager()) != null) { - startActivity(chooser); - LogUtils.d(TAG, "【分享启动】分享意图已启动,等待用户选择分享方式"); - } else { - ToastUtils.show("无可用分享应用"); - LogUtils.e(TAG, "【分享失败】未找到可响应分享的应用"); - } - } catch (Exception e) { - String errMsg = "分享异常:" + e.getMessage(); - ToastUtils.show(errMsg.substring(0, 20)); // 截取前20字,避免吐司过长 - LogUtils.d(TAG, errMsg, Thread.currentThread().getStackTrace()); - LogUtils.e(TAG, "【分享失败】分享意图启动失败,FileProvider配置或权限异常"); - } - } + // 4. 构建裁剪意图(精简参数,保留核心配置) + Intent intent = new Intent("com.android.camera.action.CROP"); + intent.setDataAndType(inputUri, "image/*"); + intent.putExtra("crop", "true"); + intent.putExtra("noFaceDetection", true); - @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 + ")"); + // 设置裁剪比例 + if (!isCropFree) { + int viewWidth = bvPreviewBackground.getWidth() > 0 ? bvPreviewBackground.getWidth() : getResources().getDisplayMetrics().widthPixels; + int viewHeight = bvPreviewBackground.getHeight() > 0 ? bvPreviewBackground.getHeight() : getResources().getDisplayMetrics().heightPixels; + int gcd = calculateGCD(viewWidth, viewHeight); + intent.putExtra("aspectX", viewWidth / gcd); + intent.putExtra("aspectY", viewHeight / gcd); + } else { + intent.putExtra("aspectX", 1); + intent.putExtra("aspectY", 1); + } - try { - // 事件分发:根据requestCode调用对应功能函数(修复:handleCropImageResult补全3个参数) - if (requestCode == REQUEST_SELECT_PICTURE) { - handleSelectPictureResult(resultCode, data); // 处理选图回调(2个参数,正常) - } else if (requestCode == REQUEST_TAKE_PHOTO) { - handleTakePhotoResult(resultCode, data); // 处理拍照回调(2个参数,正常) - } else if (requestCode == REQUEST_CROP_IMAGE) { - // 【核心修复】补全requestCode参数,与函数定义一致(3个参数:requestCode、resultCode、data) - handleCropImageResult(requestCode, resultCode, data); // 处理裁剪回调(补全参数) - } else if (resultCode != RESULT_OK) { - handleOperationCancelOrFail(); // 处理所有取消/失败场景 - } - } catch (Exception e) { - // 全局异常兜底:避免单一事件异常导致整个回调崩溃 - String errMsg = "onActivityResult 全局异常:" + e.getMessage(); - LogUtils.e(TAG, errMsg, e); - ToastUtils.show("操作失败:" + errMsg.substring(0, 20)); - // 异常时强制清理裁剪临时文件 - clearCropTempFile(); - } - } + // 设置输出尺寸(适配Android14+) + int maxOutputSize = Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU ? 1536 : 2048; + int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize); + int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize); + intent.putExtra("outputX", outputX); + intent.putExtra("outputY", outputY); + intent.putExtra("scale", true); + intent.putExtra("scaleUpIfNeeded", true); - /** - * 处理图片选择回调(REQUEST_SELECT_PICTURE) - * 职责:解析选图Uri→生成临时文件→同步预览→启动裁剪 - */ - private void handleSelectPictureResult(int resultCode, Intent data) { - // 1. 校验结果合法性 - if (resultCode != RESULT_OK || data == null) { - handleOperationCancelOrFail(); - return; - } + // 输出配置 + intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); + intent.putExtra("quality", 80); + intent.putExtra("return-data", false); // 禁用返回Bitmap,避免OOM - // 2. 解析选图Uri并添加持久化权限 - Uri selectedImage = data.getData(); - if (selectedImage == null) { - ToastUtils.show("选择的图片Uri为空"); - LogUtils.e(TAG, "【选图回调失败】选择的图片Uri为空,无法解析"); - clearCropTempFile(); - return; - } - LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString()); + // 权限Flags + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } - // 3. 适配Android4.4+,添加Uri持久化读取权限(避免后续访问无权限) - grantPersistableUriPermission(selectedImage); + // 5. 适配系统裁剪工具(显式指定Component) + try { + List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (!resolveInfos.isEmpty()) { + ResolveInfo resolveInfo = resolveInfos.get(0); + String cropPackageName = resolveInfo.activityInfo.packageName; + String cropActivityName = resolveInfo.activityInfo.name; + LogUtils.d(TAG, "【裁剪适配】找到裁剪工具:包名=" + cropPackageName + ",Activity=" + cropActivityName); - // 4. 解析Uri为文件(适配ContentProvider Uri,兜底流复制) - File selectedFile = parseUriToFile(selectedImage); - if (selectedFile == null || !selectedFile.exists() || selectedFile.length() <= 0) { - ToastUtils.show("选择的图片文件不存在或损坏"); - LogUtils.e(TAG, "【选图回调失败】解析后的图片文件无效"); - clearCropTempFile(); - return; - } + // 生成输出Uri(授予裁剪工具权限) + Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile); + // 显式设置Component,避免参数冲突 + Intent cropIntent = new Intent(intent); + cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); + cropIntent.setDataAndType(null, null); + cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + // 重新添加权限 + cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - // 5. 同步图片到预览并启动固定比例裁剪 - syncSelectedFileToPreview(selectedFile, selectedImage.toString()); - startCropImageActivity(false); - LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动固定比例裁剪"); - } + startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); + LogUtils.d(TAG, "【裁剪启动】已启动系统裁剪工具,输出路径:" + cropTempFile.getAbsolutePath()); + } else { + // 兜底:启动第三方裁剪工具 + Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropTempFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (chooser.resolveActivity(getPackageManager()) != null) { + startActivityForResult(chooser, REQUEST_CROP_IMAGE); + } else { + ToastUtils.show("无可用裁剪工具,请安装系统相机"); + mBgSourceUtils.clearCropTempFiles(); + } + } + } catch (Exception e) { + LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e); + ToastUtils.show("无法启动裁剪工具"); + mBgSourceUtils.clearCropTempFiles(); + } + } - /** - * 处理拍照回调(REQUEST_TAKE_PHOTO) - * 职责:校验拍照文件→获取Bitmap→压缩保存→同步预览→启动裁剪 - */ - private void handleTakePhotoResult(int resultCode, Intent data) { - // 1. 校验结果合法性 - if (resultCode != RESULT_OK || data == null) { - handleOperationCancelOrFail(); - return; - } + /** + * 计算最大公约数(简化裁剪比例) + */ + private int calculateGCD(int a, int b) { + if (b == 0) { + return a; + } + return calculateGCD(b, a % b); + } - // 2. 校验拍照文件有效性(核心:避免空文件/损坏文件) - if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { - ToastUtils.show("拍照文件不存在或损坏"); - LogUtils.e(TAG, "【拍照回调失败】拍照文件无效,路径:" + mfTakePhoto.getAbsolutePath()); - clearCropTempFile(); - return; - } - LogUtils.d(TAG, "【拍照校验】拍照文件有效,路径:" + mfTakePhoto.getAbsolutePath() + ",大小:" + mfTakePhoto.length() + " bytes"); + /** + * 分享图片(调用工具类获取路径,精简文件逻辑) + */ + void sharePicture() { + LogUtils.d(TAG, "【分享启动】sharePicture 触发"); + // 从工具类获取正式背景路径 + String currentBgPath = mBgSourceUtils.getCurrentBackgroundFilePath(); + File shareFile = new File(currentBgPath); + if (!shareFile.exists() || shareFile.length() <= 0) { + ToastUtils.show("分享的背景图片不存在"); + LogUtils.e(TAG, "【分享失败】分享图片无效"); + return; + } - // 3. 获取拍照Bitmap并压缩保存 - Bitmap photoBitmap = getTakePhotoBitmap(data); - if (photoBitmap == null || photoBitmap.isRecycled()) { - ToastUtils.show("拍照图片为空"); - LogUtils.e(TAG, "【拍照回调失败】拍照Bitmap为空或已回收"); - clearCropTempFile(); - return; - } - compressQualityToRecivedPicture(photoBitmap); // 复用原有压缩方法 + try { + // 生成分享Uri(调用工具类的Authority) + Uri shareUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), shareFile); + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri); + shareIntent.setType("image/jpeg"); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // 4. 刷新预览并启动固定比例裁剪 - bvPreviewBackground.reloadPreviewBackground(); - startCropImageActivity(false); - LogUtils.d(TAG, "【拍照完成】拍照回调处理结束,已启动固定比例裁剪"); - } + Intent chooser = Intent.createChooser(shareIntent, "选择分享方式"); + if (chooser.resolveActivity(getPackageManager()) != null) { + startActivity(chooser); + LogUtils.d(TAG, "【分享启动】已启动分享选择器"); + } else { + ToastUtils.show("无可用分享应用"); + } + } catch (Exception e) { + LogUtils.e(TAG, "【分享异常】分享失败:" + e.getMessage(), e); + ToastUtils.show("分享失败,请重试"); + } + } - /** - * 处理裁剪回调(REQUEST_CROP_IMAGE) - * 适配说明:兼容Java7(无Lambda/Stream,用匿名内部类实现延迟刷新),增强MIUI 0字节文件容错 - * 核心逻辑:校验裁剪文件有效性 → 处理MIUI 0字节文件 → 解析Bitmap → 保存图片 → 双重刷新预览 - * @param requestCode 回调请求码(固定为REQUEST_CROP_IMAGE=2) - * @param resultCode 结果码(RESULT_OK=-1 表示成功) - * @param data 裁剪返回数据(MIUI机型可能为null) - */ - private void handleCropImageResult(int requestCode, int resultCode, Intent data) { - // 1. 裁剪文件基础校验(Java7兼容:显式非空校验,避免空指针) - boolean isFileExist = false; - boolean isFileReadable = false; - long fileSize = 0; - if (_mSourceCropTempFile != null) { - isFileExist = _mSourceCropTempFile.exists(); - isFileReadable = isFileExist ? _mSourceCropTempFile.canRead() : false; - fileSize = isFileExist ? _mSourceCropTempFile.length() : 0; - } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + LogUtils.d(TAG, "【回调触发】onActivityResult 触发,requestCode:" + requestCode + ",resultCode:" + resultCode); + + try { + // 事件分发(仅调用对应处理方法,无文件逻辑) + if (requestCode == REQUEST_SELECT_PICTURE) { + handleSelectPictureResult(resultCode, data); + } else if (requestCode == REQUEST_TAKE_PHOTO) { + handleTakePhotoResult(resultCode, data); + } else if (requestCode == REQUEST_CROP_IMAGE) { + handleCropImageResult(requestCode, resultCode, data); + } else if (resultCode != RESULT_OK) { + handleOperationCancelOrFail(); + } + } catch (Exception e) { + LogUtils.e(TAG, "【回调异常】onActivityResult 全局异常:" + e.getMessage(), e); + ToastUtils.show("操作失败,请重试"); + mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理临时文件 + } + } + + /** + * 处理选图回调(调用工具类同步预览,精简文件操作) + */ + private void handleSelectPictureResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK || data == null) { + handleOperationCancelOrFail(); + return; + } + + Uri selectedImage = data.getData(); + if (selectedImage == null) { + ToastUtils.show("选择的图片Uri为空"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + LogUtils.d(TAG, "【选图回调】选择图片Uri : " + selectedImage.toString()); + + // 授予持久化权限 + grantPersistableUriPermission(selectedImage); + + // 解析Uri为文件(调用工具类复制文件,兜底流复制) + File selectedFile = parseUriToFile(selectedImage); + if (selectedFile == null || !selectedFile.exists()) { + ToastUtils.show("选择的图片文件无效"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 同步到预览并启动裁剪(调用工具类保存预览) + mBgSourceUtils.saveFileToPreviewBean(selectedFile, selectedImage.toString()); + bvPreviewBackground.reloadPreviewBackground(); + startCropImageActivity(false); + LogUtils.d(TAG, "【选图完成】选图回调处理结束,已启动裁剪"); + } + + /** + * 处理拍照回调(调用工具类同步预览,精简文件操作) + */ + private void handleTakePhotoResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK || data == null) { + handleOperationCancelOrFail(); + return; + } + + // 校验拍照文件有效性 + if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { + ToastUtils.show("拍照文件不存在或损坏"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 获取Bitmap并压缩保存 + Bitmap photoBitmap = getTakePhotoBitmap(data); + if (photoBitmap != null && !photoBitmap.isRecycled()) { + compressQualityToRecivedPicture(photoBitmap); + } else { + ToastUtils.show("拍照图片为空"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 同步预览并启动裁剪(调用工具类保存预览) + mBgSourceUtils.saveFileToPreviewBean(mfTakePhoto, mfTakePhoto.getAbsolutePath()); + bvPreviewBackground.reloadPreviewBackground(); + startCropImageActivity(false); + LogUtils.d(TAG, "【拍照完成】拍照回调处理结束,已启动裁剪"); + } + + /** + * 处理裁剪回调(调用工具类获取裁剪文件,精简文件操作) + */ + private void handleCropImageResult(int requestCode, int resultCode, Intent data) { + // 从工具类获取裁剪临时文件(唯一裁剪文件入口,无本地文件管理) + File cropTempFile = mBgSourceUtils.getCropTempFile(); + boolean isFileExist = cropTempFile != null && cropTempFile.exists(); + boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; + long fileSize = isFileExist ? cropTempFile.length() : 0; boolean isFileValid = isFileExist && isFileReadable && fileSize > 100; // 大于100字节视为有效 boolean isCropSuccess = (resultCode == RESULT_OK) || isFileValid; - // 打印校验日志(Java7原生字符串拼接) - String checkLog = "【裁剪回调】容错校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + - ",文件可读=" + isFileReadable + ",文件大小=" + fileSize + " bytes,是否判定为成功=" + isCropSuccess; - LogUtils.d(TAG, checkLog); + // 打印校验日志(精简版,保留核心信息) + LogUtils.d(TAG, "【裁剪回调】校验:resultCode=" + resultCode + ",文件存在=" + isFileExist + ",大小=" + fileSize + "bytes,是否成功=" + isCropSuccess); - // 2. 处理MIUI特有:文件存在但大小为0字节(裁剪工具写入失败) + // 处理MIUI 0字节文件问题 if (isFileExist && fileSize == 0) { - LogUtils.e(TAG, "【裁剪失败】裁剪临时文件为空(MIUI裁剪工具适配问题),建议选择「系统相机裁剪」或第三方裁剪工具"); + LogUtils.e(TAG, "【裁剪失败】裁剪文件为空(MIUI适配问题)"); ToastUtils.show("裁剪失败,请选择系统相机裁剪重试"); - LogUtils.d(TAG, "【裁剪调试】保留空文件用于排查:" + _mSourceCropTempFile.getPath()); - - // 【Java7兼容:0字节文件重试逻辑】重新初始化临时文件,为下次裁剪做准备 - initCropTempFileAgain(); + mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理无效文件 return; } - // 3. 处理裁剪成功场景(resultCode=RESULT_OK 或 文件有效) + // 裁剪成功:解析Bitmap并保存 if (isCropSuccess) { - // 解析裁剪临时文件为Bitmap(适配大图片OOM,Java7兼容) - Bitmap cropBitmap = parseCropTempFileToBitmap(); + Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); if (cropBitmap != null && !cropBitmap.isRecycled()) { - // 保存裁剪图片并双重刷新预览(复用原有saveCropBitmap方法,Java7兼容) - saveCropBitmap(cropBitmap); - doubleRefreshPreview(); - LogUtils.d(TAG, "【裁剪完成】裁剪回调处理结束,图片已保存并刷新预览"); + saveCropBitmap(cropBitmap); // 保存裁剪结果 + doubleRefreshPreview(); // 双重刷新预览 + LogUtils.d(TAG, "【裁剪完成】裁剪回调处理结束"); } else { - ToastUtils.show("获取剪裁图片失败(Bitmap解析异常)"); - LogUtils.e(TAG, "【裁剪回调失败】裁剪Bitmap解析失败或已回收"); - // 清理裁剪临时文件(避免残留无效文件) - clearCropTempFile(); + ToastUtils.show("获取裁剪图片失败"); + LogUtils.e(TAG, "【裁剪回调失败】Bitmap解析异常"); + mBgSourceUtils.clearCropTempFiles(); } } else { - // 4. 处理裁剪取消/失败(resultCode≠RESULT_OK 且 文件无效) + // 裁剪取消/失败:统一处理 handleOperationCancelOrFail(); } } - + /** - * 处理所有操作取消/失败(resultCode != RESULT_OK) - * 职责:统一提示+清理临时文件,避免残留文件影响下次操作 + * 处理所有操作取消/失败(统一清理+提示,无文件逻辑) */ private void handleOperationCancelOrFail() { LogUtils.d(TAG, "【操作回调】操作取消或失败"); ToastUtils.show("操作已取消"); - // 强制清理裁剪临时文件(应用私有外部目录) - clearCropTempFile(); - } - - /** - * 处理MIUI裁剪工具写入空文件(0字节)场景 - * 职责:精准提示用户+保留文件用于调试,适配MIUI裁剪特性 - */ - private void handleMiuiCropZeroByteFile() { - LogUtils.e(TAG, "【裁剪失败】裁剪临时文件为空(MIUI裁剪工具适配问题),建议选择「系统相机裁剪」或第三方裁剪工具"); - ToastUtils.show("裁剪失败,请选择系统相机裁剪重试"); - LogUtils.d(TAG, "【裁剪调试】保留空文件用于排查:" + _mSourceCropTempFile.getPath()); - // 不删除空文件,方便后续查看权限/路径问题 + mBgSourceUtils.clearCropTempFiles(); // 调用工具类清理裁剪临时文件 } /** @@ -1677,113 +796,101 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg private void grantPersistableUriPermission(Uri uri) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { getContentResolver().takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION ); - LogUtils.d(TAG, "【选图权限】已为选择的图片Uri添加持久化读取权限"); + LogUtils.d(TAG, "【选图权限】已添加持久化读取权限"); } } /** - * 辅助函数:解析选图Uri为File(适配不同Uri格式,兜底流复制) + * 辅助函数:解析选图Uri为File(调用工具类复制,精简逻辑) */ private File parseUriToFile(Uri uri) { File targetFile = null; String filePath = UriUtil.getFilePathFromUri(this, uri); - LogUtils.d(TAG, "【选图解析】Uri解析后的文件路径:" + filePath); + LogUtils.d(TAG, "【选图解析】Uri解析路径:" + filePath); - // 1. 直接解析成功 + // 直接解析成功 if (!TextUtils.isEmpty(filePath)) { targetFile = new File(filePath); } else { - // 2. 解析失败,流复制生成临时文件(应用私有外部目录,无权限问题) + // 解析失败:流复制兜底(调用工具类目录创建) targetFile = createTempFileByStreamCopy(uri); } return targetFile; } /** - * 辅助函数:通过流复制生成选图临时文件(兜底方案,适配ContentProvider Uri) + * 辅助函数:通过流复制生成选图临时文件(调用工具类管理目录) */ private File createTempFileByStreamCopy(Uri uri) { - File externalTempDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "SelectTemp"); - if (!externalTempDir.exists()) { - externalTempDir.mkdirs(); - LogUtils.d(TAG, "【选图解析】创建应用私有外部临时子目录:" + externalTempDir.getAbsolutePath()); + // 从工具类获取背景源目录,作为选图临时目录(统一路径) + File tempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp"); + if (!tempDir.exists()) { + mBgSourceUtils.copyFile(new File(""), tempDir); // 复用工具类目录创建逻辑 } - File tempFile = new File(externalTempDir, "selected_temp.jpg"); + File tempFile = new File(tempDir, "selected_temp.jpg"); try { if (tempFile.exists()) { - tempFile.delete(); - LogUtils.d(TAG, "【选图解析】旧临时文件已清理,准备生成新临时文件"); + mBgSourceUtils.clearOldFileByExternal(tempFile, "旧选图临时文件"); } - // 流复制(适配ContentProvider Uri,无需外部存储权限) + // 流复制(适配ContentProvider Uri) FileUtils.copyStreamToFile(getContentResolver().openInputStream(uri), tempFile); - LogUtils.d(TAG, "【选图解析】Uri解析失败,通过流复制生成临时文件:" + tempFile.getPath()); + LogUtils.d(TAG, "【选图解析】流复制生成临时文件:" + tempFile.getAbsolutePath()); } catch (Exception e) { - LogUtils.e(TAG, "【选图解析】流复制生成临时文件失败:" + e.getMessage(), e); + LogUtils.e(TAG, "【选图解析】流复制失败:" + e.getMessage(), e); tempFile = null; } return tempFile; } /** - * 辅助函数:同步选图文件到预览Bean并刷新预览 - */ - private void syncSelectedFileToPreview(File selectedFile, String uriStr) { - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); - utils.saveFileToPreviewBean(selectedFile, uriStr); - bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【选图同步】预览图片已更新,文件路径:" + selectedFile.getAbsolutePath()); - } - - /** - * 辅助函数:从拍照数据中获取Bitmap + * 辅助函数:从拍照数据中获取Bitmap(精简版,无文件操作) */ private Bitmap getTakePhotoBitmap(Intent data) { Bundle extras = data.getExtras(); - LogUtils.d(TAG, "【拍照回调】拍照数据Bundle:" + (extras != null ? "非空" : "为空")); if (extras == null) { return null; } Bitmap bitmap = (Bitmap) extras.get("data"); - LogUtils.d(TAG, "【拍照回调】获取拍照Bitmap:" + (bitmap != null ? "成功" : "失败") + ",是否回收:" + (bitmap != null && bitmap.isRecycled())); + LogUtils.d(TAG, "【拍照回调】获取Bitmap:" + (bitmap != null ? "成功" : "失败")); return bitmap; } /** - * 辅助函数:解析裁剪临时文件为Bitmap(适配大图片OOM,MIUI裁剪结果) + * 辅助函数:解析裁剪临时文件为Bitmap(适配大图片OOM) */ - private Bitmap parseCropTempFileToBitmap() { - Bitmap cropBitmap = null; - if (!_mSourceCropTempFile.exists() || _mSourceCropTempFile.length() <= 100) { + private Bitmap parseCropTempFileToBitmap(File cropTempFile) { + if (cropTempFile == null || !cropTempFile.exists() || cropTempFile.length() <= 100) { return null; } + Bitmap cropBitmap = null; try { BitmapFactory.Options options = new BitmapFactory.Options(); - // 第一步:仅获取图片信息,不加载Bitmap(避免OOM) + // 第一步:仅获取图片信息,不加载Bitmap options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); - LogUtils.d(TAG, "【Bitmap解析】图片信息:格式=" + (options.outMimeType != null ? options.outMimeType : "未知") + ",原始宽高=" + options.outWidth + "x" + options.outHeight); + BitmapFactory.decodeFile(cropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】图片宽高:" + options.outWidth + "x" + options.outHeight); - // 自动适配格式+计算采样率(最大边长2048,适配MIUI裁剪尺寸) + // 自动适配格式+计算采样率 options.inPreferredConfig = getBitmapConfigByMimeType(options.outMimeType); options.inSampleSize = calculateBitmapSampleRate(options, 2048); // 第二步:正式加载Bitmap options.inJustDecodeBounds = false; - cropBitmap = BitmapFactory.decodeFile(_mSourceCropTempFile.getPath(), options); - LogUtils.d(TAG, "【Bitmap解析】裁剪Bitmap加载:" + (cropBitmap != null ? "成功" : "失败") + ",加载后大小:" + (cropBitmap != null ? cropBitmap.getByteCount()/1024 + "KB" : "0")); + cropBitmap = BitmapFactory.decodeFile(cropTempFile.getPath(), options); + LogUtils.d(TAG, "【Bitmap解析】加载Bitmap:" + (cropBitmap != null ? "成功" : "失败")); } catch (Exception e) { - LogUtils.e(TAG, "【Bitmap解析】裁剪临时文件解析失败:" + e.getMessage(), e); + LogUtils.e(TAG, "【Bitmap解析】解析失败:" + e.getMessage(), e); } return cropBitmap; } /** - * 辅助函数:根据图片格式自动适配Bitmap配置(PNG保留透明,JPEG省内存) + * 辅助函数:根据图片格式适配Bitmap配置 */ private Bitmap.Config getBitmapConfigByMimeType(String mimeType) { return (mimeType != null && mimeType.contains("png")) @@ -1792,7 +899,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 辅助函数:计算Bitmap采样率(防止大图片OOM) + * 辅助函数:计算Bitmap采样率(防止OOM) */ private int calculateBitmapSampleRate(BitmapFactory.Options options, int maxSize) { int sampleRate = 1; @@ -1801,25 +908,128 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg while (width / sampleRate > maxSize || height / sampleRate > maxSize) { sampleRate *= 2; // 确保是2的幂(BitmapFactory要求) } - LogUtils.d(TAG, "【Bitmap解析】采样率计算完成:" + sampleRate + ",目标宽高=" + width/sampleRate + "x" + height/sampleRate); + LogUtils.d(TAG, "【Bitmap解析】采样率:" + sampleRate); return sampleRate; } /** - * 辅助函数:主线程双重刷新预览(适配MIUI控件渲染耗时) + * 保存裁剪后的Bitmap(调用工具类保存,精简文件逻辑) + */ + private void saveCropBitmap(Bitmap bitmap) { + LogUtils.d(TAG, "【保存启动】开始保存裁剪图片"); + if (bitmap == null || bitmap.isRecycled()) { + ToastUtils.show("裁剪图片为空"); + mBgSourceUtils.clearCropTempFiles(); + return; + } + + // 内存优化:大图片缩放 + Bitmap scaledBitmap = bitmap; + int originalSize = bitmap.getByteCount() / 1024 / 1024; // 转换为MB + if (originalSize > 10) { // 超过10MB自动缩放 + float scale = 1.0f; + while (scaledBitmap.getByteCount() / 1024 / 1024 > 5) { + scale -= 0.2f; + if (scale < 0.2f) break; + scaledBitmap = scaleBitmap(scaledBitmap, scale); + } + if (scaledBitmap != bitmap) { + bitmap.recycle(); // 回收原Bitmap + } + } + + FileOutputStream fos = null; + try { + // 从工具类获取预览压缩图路径(统一路径管理) + String scaledCompressFilePath = mBgSourceUtils.getPreviewBackgroundScaledCompressFilePath(); + File targetFile = new File(scaledCompressFilePath); + + // 确保目录存在(调用工具类) + File parentDir = targetFile.getParentFile(); + if (!parentDir.exists()) { + mBgSourceUtils.copyFile(new File(""), parentDir); + } + + // 清理旧文件(调用工具类) + if (targetFile.exists()) { + mBgSourceUtils.clearOldFileByExternal(targetFile, "旧裁剪结果文件"); + } + targetFile.createNewFile(); + mBgSourceUtils.setFilePermissions(targetFile); // 调用工具类设置权限 + + // 压缩保存 + fos = new FileOutputStream(targetFile); + Bitmap.CompressFormat format = targetFile.getName().endsWith(".png") ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG; + boolean success = scaledBitmap.compress(format, 80, fos); + fos.flush(); + fos.getFD().sync(); + + if (success) { + ToastUtils.show("图片保存成功"); + // 同步预览Bean(调用工具类) + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath); + previewBean.setIsUseBackgroundFile(true); + mBgSourceUtils.saveSettings(); + + // 同步文件到预览目录(调用工具类) + mBgSourceUtils.saveFileToPreviewBean(targetFile, scaledCompressFilePath); + } else { + ToastUtils.show("图片保存失败"); + LogUtils.e(TAG, "【保存失败】Bitmap压缩失败"); + // 回滚配置 + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setIsUseBackgroundFile(false); + mBgSourceUtils.saveSettings(); + } + } catch (Exception e) { + LogUtils.e(TAG, "【保存异常】" + e.getMessage(), e); + ToastUtils.show("图片保存失败"); + } finally { + // 关闭流 + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【保存异常】流关闭失败:" + e.getMessage()); + } + } + // 回收Bitmap + if (scaledBitmap != null && !scaledBitmap.isRecycled()) { + scaledBitmap.recycle(); + } + // 清理裁剪临时文件(调用工具类) + mBgSourceUtils.clearCropTempFiles(); + } + } + + /** + * 辅助函数:缩放Bitmap(适配大图片) + */ + private Bitmap scaleBitmap(Bitmap bitmap, float scale) { + if (bitmap == null || scale <= 0 || scale >= 1.0f) { + return bitmap; + } + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + + /** + * 辅助函数:双重刷新预览(适配MIUI渲染延迟) */ private void doubleRefreshPreview() { runOnUiThread(new Runnable() { @Override public void run() { bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第一次刷新bvPreviewBackground(立即)"); - // 延迟300ms再次刷新,确保MIUI机型加载成功 + LogUtils.d(TAG, "【预览刷新】第一次刷新(立即)"); + // 延迟300ms再次刷新 new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【预览刷新】第二次刷新bvPreviewBackground(延迟300ms)"); + LogUtils.d(TAG, "【预览刷新】第二次刷新(延迟300ms)"); } }, 300); } @@ -1827,328 +1037,113 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } /** - * 辅助函数:清理裁剪临时文件(统一封装,避免重复代码) - */ - private void clearCropTempFile() { - if (_mSourceCropTempFile.exists()) { - boolean deleteSuccess = _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【临时文件清理】裁剪临时文件清理:" + (deleteSuccess ? "成功" : "失败") + ",路径:" + _mSourceCropTempFile.getPath()); - } - } - - - /** - * 检查类型是否为图片(适配常见图片格式,避免非图片文件误处理) + * 检查类型是否为图片(精简版,保留核心校验) */ 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; + return type.startsWith("image/") || "image/jpeg".equals(type) || "image/png".equals(type); } /** - * 检查并申请存储权限(终极修复:适配Android 14+ 应用私有外部目录权限,解决Permission denied) - * 关键适配:Android14+ 需显式申请WRITE_EXTERNAL_STORAGE,仅靠所有文件访问权限无法覆盖应用私有目录 + * 检查并申请存储权限(精简版,仅保留核心权限校验) */ private boolean checkAndRequestStoragePermission() { LogUtils.d(TAG, "【权限校验】checkAndRequestStoragePermission 触发,Android版本:" + Build.VERSION.SDK_INT); - // 核心修复1:Android 14+(API 34+)强制申请 WRITE_EXTERNAL_STORAGE 权限(应用私有外部目录必需) - if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { // Android 13+ 开始强化,14+ 强制要求 + // Android14+:申请WRITE_EXTERNAL_STORAGE + if (Build.VERSION.SDK_INT >= Build_VERSION_CODES_TIRAMISU) { boolean hasWritePerm = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - LogUtils.d(TAG, "【权限校验】Android14+ 应用私有外部目录必需:WRITE_EXTERNAL_STORAGE 权限=" + hasWritePerm); if (!hasWritePerm) { - // 显式申请 WRITE_EXTERNAL_STORAGE 权限(适配应用私有外部目录) ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST); - LogUtils.d(TAG, "【权限申请】Android14+ 已触发 WRITE_EXTERNAL_STORAGE 权限申请(应用私有目录必需)"); return false; } } - // 核心修复2:Android 11+(R)保留所有文件访问权限校验(覆盖外部存储其他目录) + // Android11+:检查所有文件访问权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { boolean hasAllFilePerm = Environment.isExternalStorageManager(); - LogUtils.d(TAG, "【权限校验】Android11+ 所有文件访问权限:" + (hasAllFilePerm ? "已获取" : "未获取") + "(外部存储其他目录必需)"); if (!hasAllFilePerm) { - Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - startActivity(intent); - ToastUtils.show("请开启「所有文件访问权限」+「存储写入权限」(图片选择/裁剪必需)"); + startActivity(new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)); + ToastUtils.show("请开启「所有文件访问权限」"); return false; } } - // Android 6.0-10:保留原读写权限校验(适配旧机型) + // Android6.0-10:申请读写权限 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { boolean hasReadPerm = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; boolean hasWritePerm = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - LogUtils.d(TAG, "【权限校验】Android6.0-10 存储权限:读=" + hasReadPerm + ",写=" + hasWritePerm); if (!hasReadPerm || !hasWritePerm) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST); - LogUtils.d(TAG, "【权限申请】已触发存储读写权限申请"); return false; } } - // 修复3:权限校验通过后,强制刷新应用私有目录权限(解决Android14+ 权限设置后不生效问题) - refreshAppPrivateDirPermission(); - - LogUtils.d(TAG, "【权限校验】存储权限校验完成(Android14+ 应用私有目录权限已确认),可正常操作"); + LogUtils.d(TAG, "【权限校验】存储权限校验通过"); return true; } /** - * 辅助函数:强制刷新应用私有目录权限(解决Android14+ 权限设置后不生效问题) - */ - private void refreshAppPrivateDirPermission() { - File appPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); - if (appPrivateDir != null && appPrivateDir.exists()) { - // 递归设置目录权限(Android14+ 必需,单一目录设置无效) - setDirPermissionsRecursively(appPrivateDir); - LogUtils.d(TAG, "【权限刷新】应用私有目录权限已强制设置:" + appPrivateDir.getAbsolutePath() + ",可写=" + appPrivateDir.canWrite() + ",可读=" + appPrivateDir.canRead()); - } - } - - /** - * 递归设置目录及子目录/文件的读写权限(适配Java7 + Android全版本,解决Permission denied问题) - * 核心适配:移除Java8+特性(如Lambda、方法引用),兼容Java7语法,同时保障Android14+权限有效性 - * @param dir 要设置权限的目标目录 - */ - private void setDirPermissionsRecursively(File dir) { - // 1. 校验目录有效性(避免空指针/无效目录,Java7兼容写法) - if (dir == null || !dir.exists()) { - String dirPath = (dir != null) ? dir.getAbsolutePath() : "null"; - LogUtils.d(TAG, "【权限设置】目录无效,无需处理:" + dirPath); - return; - } - - try { - // 2. 核心:设置目录权限(Android14+ 关键,Java7兼容写法) - // 注:setReadable/setWritable 第二个参数(ownerOnly)Java7已支持,必须设为false(允许所有用户访问) - dir.setReadable(true, false); // 所有用户可读取(裁剪工具需读取文件) - dir.setWritable(true, false); // 所有用户可写入(裁剪工具需写入文件) - dir.setExecutable(false, false); // 关闭执行权限,提升安全性(目录无需执行) - - // 3. 打印目录权限状态(Java7字符串拼接写法,避免String.join) - String dirPath = dir.getAbsolutePath(); - boolean canWrite = dir.canWrite(); - boolean canRead = dir.canRead(); - String dirType = getDirTypeDesc(dir); - LogUtils.d(TAG, "【权限设置】目录权限更新完成:路径=" + dirPath + ",可写=" + canWrite + ",可读=" + canRead + ",目录类型=" + dirType); - - // 4. 递归处理子目录和文件(Java7兼容,避免Stream API) - File[] files = dir.listFiles(); - // 校验files非空(避免空指针,Java7必需) - if (files != null && files.length > 0) { - // 增强for循环(Java7支持,替代forEach) - for (File file : files) { - if (file.isDirectory()) { - // 递归处理子目录(确保所有层级权限一致) - setDirPermissionsRecursively(file); - } else { - // 处理文件:设置与目录一致的权限(Java7兼容) - file.setReadable(true, false); - file.setWritable(true, false); - file.setExecutable(false, false); - - // 5. 关键文件(裁剪临时/结果文件)单独打印日志(精准调试) - String fileName = file.getName(); - if (fileName.equals(_mSourceCropTempFileName) || fileName.equals(_mSourceCroppedFileName)) { - String filePath = file.getAbsolutePath(); - boolean fileCanWrite = file.canWrite(); - boolean fileCanRead = file.canRead(); - LogUtils.d(TAG, "【权限设置】关键文件权限更新:文件名=" + fileName + ",路径=" + filePath + ",可写=" + fileCanWrite + ",可读=" + fileCanRead); - } - } - } - } - - } catch (SecurityException e) { - // 捕获系统权限异常(如系统禁止修改权限,Java7兼容捕获) - String errMsg = "【权限设置异常】系统禁止修改目录权限:" + dir.getAbsolutePath() + ",错误信息:" + e.getMessage(); - LogUtils.e(TAG, errMsg, e); - // 提示用户(Java7兼容:用Runnable替代Lambda) - runOnUiThread(new Runnable() { - @Override - public void run() { - ToastUtils.show("文件权限设置失败,请检查应用存储权限"); - } - }); - - } catch (Exception e) { - // 捕获通用异常(文件锁定、IO错误等,Java7兼容) - String errMsg = "【权限设置异常】处理目录权限时出错:" + dir.getAbsolutePath() + ",错误信息:" + e.getMessage(); - LogUtils.e(TAG, errMsg, e); - } - } - - /** - * 辅助函数:获取目录类型描述(适配Java7+Android全版本,精准识别应用内目录) - * 核心特性: - * 1. 兼容Java7语法(无Lambda、Stream、Optional,仅用if-else/显式非空校验) - * 2. 优先识别应用内目录(getFilesDir()),精准匹配裁剪临时文件存储目录 - * 3. 显式处理空指针(所有目录对象先校验非空,避免NullPointerException) - * 4. 清晰区分4类目录(应用内/应用私有外部/缓存/外部存储),便于日志调试 - * @param dir 目标目录(需传入裁剪临时文件/背景文件的父目录) - * @return 目录类型描述(如"应用内目录(getFilesDir(),裁剪临时文件存储目录)") - */ - private String getDirTypeDesc(File dir) { - // 1. 空目录校验(Java7必需,避免空指针异常) - if (dir == null) { - return "未知目录(传入目录为null)"; - } - - // 2. 定义目录路径变量(Java7:提前定义,避免重复调用方法,提升性能) - String dirPath = dir.getAbsolutePath(); - String internalDirPath = ""; // 应用内目录路径(getFilesDir()) - String externalPrivateDirPath = ""; // 应用私有外部目录路径(getExternalFilesDir()) - String cacheDirPath = ""; // 应用缓存目录路径(getCacheDir()) - - // 3. 目录路径赋值(Java7:显式非空校验,避免getFilesDir()等方法返回null导致异常) - File internalDir = getFilesDir(); - if (internalDir != null) { - internalDirPath = internalDir.getAbsolutePath(); - } - - File externalPrivateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); - if (externalPrivateDir != null) { - externalPrivateDirPath = externalPrivateDir.getAbsolutePath(); - } - - File cacheDir = getCacheDir(); - if (cacheDir != null) { - cacheDirPath = cacheDir.getAbsolutePath(); - } - - // 4. 目录类型判断(Java7:用if-else顺序判断,优先识别应用内目录,避免匹配冲突) - // 优先级1:应用内目录(getFilesDir(),裁剪临时文件默认存储目录) - if (!TextUtils.isEmpty(internalDirPath) && dirPath.contains(internalDirPath)) { - return "应用内目录(getFilesDir(),裁剪临时文件存储目录,无需外部权限)"; - } - // 优先级2:应用私有外部目录(getExternalFilesDir(),背景文件存储目录) - else if (!TextUtils.isEmpty(externalPrivateDirPath) && dirPath.contains(externalPrivateDirPath)) { - return "应用私有外部目录(getExternalFilesDir(),背景文件存储目录)"; - } - // 优先级3:应用缓存目录(getCacheDir(),兜底目录) - else if (!TextUtils.isEmpty(cacheDirPath) && dirPath.contains(cacheDirPath)) { - return "应用缓存目录(getCacheDir(),临时文件兜底目录)"; - } - // 优先级4:外部存储目录(需额外权限,功能受限) - else { - return "外部存储目录(非应用私有目录,需额外存储权限,功能可能受限)"; - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - LogUtils.d(TAG, "【权限回调】onRequestPermissionsResult 触发,requestCode:" + requestCode); - // 处理存储权限申请结果(仅针对外部存储权限,缓存目录无需) - if (requestCode == STORAGE_PERMISSION_REQUEST) { - boolean isGranted = false; - // 校验所有申请的权限是否都通过(读写权限需同时授予) - for (int i = 0; i < grantResults.length; i++) { - String perm = permissions[i]; - int result = grantResults[i]; - LogUtils.d(TAG, "【权限回调】权限:" + perm + ",申请结果:" + (result == PackageManager.PERMISSION_GRANTED ? "通过" : "拒绝") + "(仅影响外部存储操作)"); - if (result == PackageManager.PERMISSION_GRANTED) { - isGranted = true; - } else { - // 只要有一个权限拒绝,标记为未通过(读写权限缺一不可) - isGranted = false; - break; - } - } - if (isGranted) { - ToastUtils.show("存储权限已获取,正在打开图片选择器"); - LogUtils.d(TAG, "【权限回调】存储权限全部通过(外部存储可用),自动重试图片选择"); - // 核心优化:自动重试图片选择(无需用户再次点击按钮) - onSelectPictureClickListener.onClick(findViewById(R.id.activitybackgroundpictureAButton2)); - } else { - ToastUtils.show("需要存储权限才能选择/保存外部图片(缓存目录操作不受影响)"); - LogUtils.e(TAG, "【权限回调】存储权限申请被拒绝,外部图片功能受限(缓存目录操作正常)"); - } - } - } - - /** - * 设置页面背景颜色(适配像素拾取功能,实时更新纯色背景) + * 设置页面背景颜色(仅操作控件,无文件逻辑) */ 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) + ")"); + LogUtils.d(TAG, "【背景设置】setBackgroundColor 触发"); + BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); + int pixelColor = bean.getPixelColor(); + LogUtils.d(TAG, "【背景设置】当前像素颜色:" + pixelColor); RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1); if (mainLayout != null) { - mainLayout.setBackgroundColor(nPixelColor); - LogUtils.d(TAG, "【背景设置】页面背景颜色更新完成"); + mainLayout.setBackgroundColor(pixelColor); } else { - LogUtils.e(TAG, "【背景设置】主布局控件为空,无法更新背景颜色"); + LogUtils.e(TAG, "【背景设置】主布局为空,无法更新颜色"); } } @Override protected void onResume() { super.onResume(); - LogUtils.d(TAG, "【生命周期】onResume 触发,刷新页面状态"); - // 恢复时更新纯色背景(避免后台切换后颜色显示异常) + LogUtils.d(TAG, "【生命周期】onResume 触发"); + // 刷新背景颜色+预览 setBackgroundColor(); - // Resume时刷新预览(避免后台切换后图片显示异常) bvPreviewBackground.reloadPreviewBackground(); - LogUtils.d(TAG, "【生命周期】onResume 完成,背景颜色和预览已刷新"); + LogUtils.d(TAG, "【生命周期】onResume 完成"); } /** - * 显示网络图片下载对话框(触发网络背景设置) + * 显示网络图片下载对话框(仅UI逻辑) */ public void onNetworkBackgroundDialog(View view) { - LogUtils.d(TAG, "【网络图片】onNetworkBackgroundDialog 触发,显示网络图片下载对话框"); - // 初始化网络图片对话框(传入点击回调) + 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); - // 保存预览资源信息(用于后续预览和裁剪) + LogUtils.d(TAG, "【网络图片】用户确认下载:" + szConfirmFilePath); preViewFilePath = szConfirmFilePath; preViewFileUrl = szConfirmFileUrl; - // 触发图片接收逻辑 onRecivedPictureListener.onRecivedPicture(preViewFilePath, preViewFileUrl); } @Override public void onCancel() { - LogUtils.d(TAG, "【网络图片】用户取消网络图片下载"); - // 取消逻辑:无需额外操作,关闭对话框即可 + LogUtils.d(TAG, "【网络图片】用户取消下载"); } }); - - // 可选:修改对话框标题和内容(适配自定义场景,提升用户体验) - dialog.setTitle("网络图片下载对话框"); - dialog.setContent("是否下载该地址的图片,作为应用背景?"); - LogUtils.d(TAG, "【网络图片】网络图片对话框配置完成,准备显示"); - - // 显示对话框 + dialog.setTitle("网络图片下载"); + dialog.setContent("是否下载该图片作为背景?"); dialog.show(); - LogUtils.d(TAG, "【网络图片】网络图片对话框已显示"); } /** - * 图片接收监听器(处理网络图片/分享图片的接收逻辑) + * 图片接收监听器(调用工具类同步预览,无文件逻辑) */ interface OnRecivedPictureListener { void onRecivedPicture(String srcFilePath, String srcFileUrl); } -// 图片接收监听器实现(网络图片下载/分享图片后的后续处理,适配缓存目录) + // 图片接收监听器实现(精简版,依赖工具类) OnRecivedPictureListener onRecivedPictureListener = new OnRecivedPictureListener(){ @Override public void onRecivedPicture(String srcFilePath, String srcFileUrl) { @@ -2159,28 +1154,27 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.e(TAG, "【图片接收失败】图片路径为空,无法处理"); return; } - // 校验文件是否存在且有效 + // 校验文件是否存在且有效(依赖工具类目录逻辑,无本地文件操作) File srcFile = new File(srcFilePath); - LogUtils.d(TAG, "【图片接收校验】图片文件:路径=" + srcFile.getAbsolutePath() + ",是否存在=" + srcFile.exists() + ",文件大小=" + srcFile.length() + " bytes,是否缓存目录:" + srcFile.getAbsolutePath().contains(getCacheDir().getAbsolutePath())); + 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); + // 同步图片到预览Bean并刷新(调用工具类统一处理,无本地文件复制) + mBgSourceUtils.saveFileToPreviewBean(srcFile, srcFileUrl); LogUtils.d(TAG, "【图片接收】图片已同步到预览Bean,刷新预览视图"); - // 修复:网络图片下载后刷新预览(确保图片正常显示) + // 刷新预览(确保图片正常显示) bvPreviewBackground.reloadPreviewBackground(); - // 启动自由裁剪(网络图片适配自由比例调整,裁剪临时文件存入缓存目录) + // 启动自由裁剪(调用工具类获取裁剪路径,无本地文件管理) startCropImageActivity(true); - LogUtils.d(TAG, "【图片接收】已启动自由裁剪,临时文件将存入应用缓存目录"); + LogUtils.d(TAG, "【图片接收】已启动自由裁剪,裁剪路径由工具类管理"); } }; /** - * 重写finish方法,确保所有退出场景都触发确认提示(避免用户误操作丢失配置) + * 重写finish方法,确保所有退出场景都触发确认提示(仅操作Bean,无文件逻辑) */ @Override public void finish() { @@ -2192,83 +1186,99 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onNo() { - // 用户选择“不应用”:保留原正式背景,仅更新启用状态 + // 用户选择“不应用”:保留原正式背景,仅更新启用状态(依赖工具类Bean) LogUtils.d(TAG, "【退出确认】用户选择:不应用预览图片,保留原背景配置"); isCommitSettings = true; // 标记为已提交,避免重复弹窗 - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - BackgroundBean currentBean = utils.getCurrentBackgroundBean(); - // 修复:根据预览路径是否为空,同步正式背景启用状态(避免残留无效配置) + BackgroundBean currentBean = mBgSourceUtils.getCurrentBackgroundBean(); + // 根据预览路径是否为空,同步正式背景启用状态(避免残留无效配置) currentBean.setIsUseBackgroundFile(!TextUtils.isEmpty(preViewFilePath)); - utils.saveSettings(); // 持久化原配置(仅更新启用状态,不修改图片路径) + mBgSourceUtils.saveSettings(); // 调用工具类持久化原配置(仅更新启用状态) LogUtils.d(TAG, "【退出配置】原背景配置保存完成,正式背景启用状态:" + currentBean.isUseBackgroundFile()); - // 退出前清理缓存目录下的所有临时文件(释放缓存空间) - clearCacheDirTempFiles(); + // 退出前清理裁剪临时文件(调用工具类统一清理,无本地文件删除) + mBgSourceUtils.clearCropTempFiles(); finish(); } @Override public void onYes() { - // 用户选择“应用”:将预览背景深拷贝到正式背景,覆盖原配置 + // 用户选择“应用”:将预览背景深拷贝到正式背景(调用工具类统一处理) LogUtils.d(TAG, "【退出确认】用户选择:应用预览图片到正式背景"); - BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(BackgroundSettingsActivity.this); - utils.commitPreviewSourceToCurrent(); // 核心:提交预览配置(深拷贝,避免数据污染) + mBgSourceUtils.commitPreviewSourceToCurrent(); // 核心:工具类处理深拷贝+持久化 isCommitSettings = true; // 标记为已提交 LogUtils.d(TAG, "【退出配置】预览背景提交完成,正式背景已更新"); ToastUtils.show("背景图片应用成功"); // 补充用户反馈,明确操作结果 - // 退出前清理缓存目录下的所有临时文件(释放缓存空间) - clearCacheDirTempFiles(); + // 退出前清理裁剪临时文件(调用工具类统一清理) + mBgSourceUtils.clearCropTempFiles(); finish(); } }); } else { // 已提交配置(或用户已选择弹窗选项),直接执行退出,避免循环 LogUtils.d(TAG, "【生命周期】已提交配置,执行super.finish()正常退出"); - // 退出前清理缓存目录下的所有临时文件(释放缓存空间) - clearCacheDirTempFiles(); + // 退出前清理裁剪临时文件(调用工具类统一清理) + mBgSourceUtils.clearCropTempFiles(); super.finish(); } } /** - * 清理应用缓存目录下的所有临时文件(裁剪/选择图片产生的临时文件) + * 权限申请回调(仅处理权限结果,无文件逻辑) */ - private void clearCacheDirTempFiles() { - LogUtils.d(TAG, "【缓存清理】clearCacheDirTempFiles 触发,清理应用缓存目录下的临时文件"); - // 清理裁剪临时目录 - File cropTempDir = new File(getCacheDir(), "CropTemp"); - deleteDirFiles(cropTempDir); - // 清理选择图片临时目录 - File selectTempDir = new File(getCacheDir(), "SelectTemp"); - deleteDirFiles(selectTempDir); - // 清理单独的裁剪临时文件(兼容可能的零散文件) - if (_mSourceCropTempFile.exists()) { - _mSourceCropTempFile.delete(); - LogUtils.d(TAG, "【缓存清理】单独裁剪临时文件清理完成:" + _mSourceCropTempFile.getAbsolutePath()); - } - LogUtils.d(TAG, "【缓存清理】应用缓存目录临时文件清理完成"); - } - - /** - * 递归删除目录下的所有文件(保留目录本身,避免误删目录) - * @param dir 要清理的目录 - */ - private void deleteDirFiles(File dir) { - if (dir == null || !dir.exists() || !dir.isDirectory()) { - LogUtils.d(TAG, "【缓存清理】目录无效,无需清理:" + (dir != null ? dir.getAbsolutePath() : "null")); - return; - } - File[] files = dir.listFiles(); - if (files == null || files.length == 0) { - LogUtils.d(TAG, "【缓存清理】目录为空,无需清理:" + dir.getAbsolutePath()); - return; - } - for (File file : files) { - if (file.isFile()) { - boolean deleteSuccess = file.delete(); - LogUtils.d(TAG, "【缓存清理】删除缓存文件:" + file.getAbsolutePath() + ",结果:" + (deleteSuccess ? "成功" : "失败")); - } else if (file.isDirectory()) { - deleteDirFiles(file); // 递归清理子目录 + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + LogUtils.d(TAG, "【权限回调】onRequestPermissionsResult 触发,requestCode:" + requestCode); + // 处理存储权限申请结果 + if (requestCode == STORAGE_PERMISSION_REQUEST) { + boolean isGranted = false; + // 校验所有申请的权限是否都通过(读写权限需同时授予) + for (int i = 0; i < grantResults.length; i++) { + String perm = permissions[i]; + int result = grantResults[i]; + LogUtils.d(TAG, "【权限回调】权限:" + perm + ",申请结果:" + (result == PackageManager.PERMISSION_GRANTED ? "通过" : "拒绝")); + if (result == PackageManager.PERMISSION_GRANTED) { + isGranted = true; + } else { + isGranted = false; + break; + } + } + if (isGranted) { + ToastUtils.show("存储权限已获取,正在重试操作"); + LogUtils.d(TAG, "【权限回调】存储权限全部通过,自动重试上次操作"); + // 自动重试图片选择(无需用户再次点击按钮) + onSelectPictureClickListener.onClick(findViewById(R.id.activitybackgroundpictureAButton2)); + } else { + ToastUtils.show("需要存储权限才能选择/保存图片"); + LogUtils.e(TAG, "【权限回调】存储权限申请被拒绝,图片功能受限"); } } } + + /** + * 兼容父类方法(若有重写需求,保留空实现或精简逻辑) + */ + @Override + public void onBackPressed() { + LogUtils.d(TAG, "【生命周期】onBackPressed 触发,执行finish"); + finish(); + } + + /** + * 避免内存泄漏:清空工具类引用(可选,根据父类生命周期规范补充) + */ + @Override + protected void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "【生命周期】onDestroy 触发,清理资源"); + // 清空工具类引用(避免Activity销毁后持有引用导致内存泄漏) + if (mBgSourceUtils != null) { + mBgSourceUtils.clearCropTempFiles(); // 退出前最后清理一次临时文件 + } + mBgSourceUtils = null; + // 清空控件引用 + bvPreviewBackground = null; + mAToolbar = null; + LogUtils.d(TAG, "【生命周期】onDestroy 完成,资源清理完毕"); + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java index 23966d50..cc89fd18 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java @@ -40,7 +40,7 @@ public class BackgroundPicturePreviewDialog extends Dialog { initEnv(); mContext = context; - mBackgroundPictureUtils = ((BackgroundSettingsActivity)context).mBackgroundSourceUtils; + mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext); ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1); copyAndViewRecivePicture(imageView); @@ -95,7 +95,7 @@ public class BackgroundPicturePreviewDialog extends Dialog { File fSrcImage = new File(szSrcImage); //mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName(); - File mfPreReceivedPhoto = new File(activity.mBackgroundSourceUtils.getBackgroundSourceDirPath(), mszPreReceivedFileName); + File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName); // 复制源图片到剪裁文件 try { FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto); diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java index f187e732..ea5ec303 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -1,19 +1,33 @@ package cc.winboll.studio.powerbell.utils; import android.content.Context; +import android.os.Environment; +import android.text.TextUtils; +import cc.winboll.studio.powerbell.BuildConfig; import cc.winboll.studio.powerbell.model.BackgroundBean; -import java.io.File; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; +import java.io.File; +import java.io.IOException; /** * @Author ZhanGSKen * @Date 2024/07/18 12:07:20 - * @Describe 背景图片工具集(修复单例模式,线程安全+数据流转正常) + * @Describe 背景图片工具集(全量文件管理+裁剪FileProvider路径适配,线程安全+数据流转正常) + * 核心能力: + * 1. 统一管理所有文件路径(背景图/裁剪临时文件/压缩图) + * 2. 为系统裁剪应用创建可读写的FileProvider路径(适配Android14+ MIUI) + * 3. 替代BackgroundSettingsActivity的所有文件操作逻辑 */ public class BackgroundSourceUtils { public static final String TAG = "BackgroundPictureUtils"; + // 裁剪相关常量(统一定义,避免硬编码) + private static final String CROP_TEMP_DIR_NAME = "CropTemp"; // 裁剪临时目录(FileProvider适配) + private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; // 裁剪输入临时文件 + private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; // 裁剪输出结果文件 + private static final String CROP_FALLBACK_DIR_NAME = "CropFallback"; // 裁剪兜底目录 + private static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容 // 1. 静态实例加volatile,禁止指令重排,保证可见性(双重校验锁单例核心) private static volatile BackgroundSourceUtils sInstance; @@ -22,13 +36,17 @@ public class BackgroundSourceUtils { private BackgroundBean currentBackgroundBean; private File previewBackgroundBeanFile; private BackgroundBean previewBackgroundBean; - // 应用外部存储文件夹路径(按功能划分目录,避免路径混乱) - private File fUtilsDir; - private File fModelDir; - // 背景图片源文件目录(存储正式/预览图片) - private File fBackgroundSourceDir; - // 2. 私有构造器(加防反射逻辑+初始化目录) + // 2. 统一文件目录(全量文件管理,替代Activity中的目录变量) + private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundPictureUtils) + private File fModelDir; // 模型文件目录(存储BackgroundBean的JSON文件) + private File fBackgroundSourceDir; // 背景图片源目录(存储正式/预览图片) + private File fCropTempDir; // 裁剪临时目录(FileProvider适配路径,系统裁剪应用可读写) + private File fCropFallbackDir; // 裁剪兜底目录(应用私有外部目录失败时使用) + private File cropTempFile; // 裁剪临时文件(系统裁剪应用写入目标) + private File cropResultFile; // 裁剪结果文件(裁剪后保存的最终文件) + + // 3. 私有构造器(加防反射逻辑+初始化所有目录/文件) private BackgroundSourceUtils(Context context) { // 防反射破坏:若已有实例,抛异常阻止重复创建 if (sInstance != null) { @@ -36,13 +54,15 @@ public class BackgroundSourceUtils { } // 上下文用Application Context,避免Activity内存泄漏 this.mContext = context.getApplicationContext(); - // 初始化目录(按功能划分,确保目录存在) - initDirs(); + // 初始化所有目录(文件管理核心:统一创建+权限设置) + initAllDirs(); + // 初始化所有文件(裁剪临时文件/结果文件等) + initAllFiles(); // 加载配置(正式/预览Bean) loadSettings(); } - // 3. 双重校验锁单例(线程安全,高效,支持多线程并发调用) + // 4. 双重校验锁单例(线程安全,高效,支持多线程并发调用) public static BackgroundSourceUtils getInstance(Context context) { // 第一重校验:避免每次调用都加锁(提升效率) if (sInstance == null) { @@ -58,33 +78,110 @@ public class BackgroundSourceUtils { } /** - * 初始化所有目录(确保目录存在,避免文件操作失败) + * 初始化所有文件目录(统一管理,替代Activity中的目录初始化逻辑) + * 包含:工具类根目录、模型目录、背景图目录、裁剪临时目录、裁剪兜底目录 */ - private void initDirs() { - // 工具类根目录(外部存储:/Android/data/包名/files/BackgroundPictureUtils) + private void initAllDirs() { + // 1. 工具类根目录(应用外部存储:/Android/data/包名/files/BackgroundPictureUtils) fUtilsDir = mContext.getExternalFilesDir(TAG); if (fUtilsDir == null) { - LogUtils.e(TAG, "外部存储不可用,无法初始化目录"); - return; - } - // 模型文件目录(存储BackgroundBean的JSON文件) - fModelDir = new File(fUtilsDir, "ModelDir"); - // 背景图片源目录(存储正式/预览图片文件) - fBackgroundSourceDir = new File(fUtilsDir, "BackgroundSource"); - - // 递归创建所有目录(确保目录存在) - if (!fModelDir.exists()) { - fModelDir.mkdirs(); - LogUtils.d(TAG, "创建模型文件目录:" + fModelDir.getAbsolutePath()); - } - if (!fBackgroundSourceDir.exists()) { - fBackgroundSourceDir.mkdirs(); - LogUtils.d(TAG, "创建背景图片目录:" + fBackgroundSourceDir.getAbsolutePath()); + LogUtils.e(TAG, "【文件管理】应用外部存储不可用,切换到应用内部缓存目录"); + fUtilsDir = mContext.getCacheDir(); // 极端兜底:应用内部缓存目录 } - // 初始化Bean文件对象(存储JSON配置) + // 2. 子目录初始化(按功能划分,确保目录存在并设置权限) + fModelDir = new File(fUtilsDir, "ModelDir"); // 模型文件目录(JSON配置) + fBackgroundSourceDir = new File(fUtilsDir, "BackgroundSource"); // 背景图片目录 + fCropTempDir = new File(fUtilsDir, CROP_TEMP_DIR_NAME); // 裁剪临时目录(FileProvider适配路径) + fCropFallbackDir = new File(fUtilsDir, CROP_FALLBACK_DIR_NAME); // 裁剪兜底目录 + + // 3. 递归创建所有目录(确保目录存在,Android14+ 必需) + createDirWithPermission(fModelDir, "模型文件目录"); + createDirWithPermission(fBackgroundSourceDir, "背景图片目录"); + createDirWithPermission(fCropTempDir, "裁剪临时目录(FileProvider适配)"); + createDirWithPermission(fCropFallbackDir, "裁剪兜底目录"); + + // 4. 初始化Bean文件对象(存储JSON配置) currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json"); previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json"); + + LogUtils.d(TAG, "【文件管理】所有目录初始化完成:根目录=" + fUtilsDir.getAbsolutePath()); + } + + /** + * 初始化所有文件(统一管理,替代Activity中的文件初始化逻辑) + * 包含:裁剪临时文件、裁剪结果文件 + */ + private void initAllFiles() { + // 1. 裁剪临时文件(系统裁剪应用写入路径,优先裁剪临时目录) + cropTempFile = new File(fCropTempDir, CROP_TEMP_FILE_NAME); + // 2. 裁剪结果文件(裁剪后保存的最终文件,存入背景图片目录) + cropResultFile = new File(fBackgroundSourceDir, CROP_RESULT_FILE_NAME); + + // 3. 初始化时清理旧文件(避免文件锁定/权限残留) + clearOldFile(cropTempFile, "旧裁剪临时文件"); + clearOldFile(cropResultFile, "旧裁剪结果文件"); + + LogUtils.d(TAG, "【文件管理】所有文件初始化完成:裁剪临时文件=" + cropTempFile.getAbsolutePath() + ",裁剪结果文件=" + cropResultFile.getAbsolutePath()); + } + + /** + * 核心函数:为系统裁剪应用创建可读写的FileProvider路径(满足需求) + * 功能:确保裁剪临时目录(fCropTempDir)可读写,返回裁剪临时文件(系统裁剪应用写入目标) + * 适配:Android14+、MIUI,解决Permission denied问题,确保系统裁剪应用能正常写入 + * @return 裁剪临时文件(File),系统裁剪应用可读写,路径已适配FileProvider + */ + public File createCropFileProviderPath() { + LogUtils.d(TAG, "【裁剪路径】createCropFileProviderPath 触发,创建系统裁剪可读写路径"); + + // 1. 优先尝试裁剪临时目录(FileProvider适配路径) + if (isDirActuallyWritable(fCropTempDir)) { + try { + // 重新初始化裁剪临时文件(先删后建,避免文件锁定) + clearOldFile(cropTempFile, "裁剪临时文件(重新初始化)"); + cropTempFile.createNewFile(); + // 强制设置文件权限(系统裁剪应用必需:允许所有用户读写) + setFilePermissions(cropTempFile); + LogUtils.d(TAG, "【裁剪路径】系统裁剪可读写路径创建成功(裁剪临时目录):" + cropTempFile.getAbsolutePath()); + return cropTempFile; + } catch (IOException e) { + LogUtils.e(TAG, "【裁剪路径】裁剪临时目录创建文件失败:" + e.getMessage(), e); + } + } + + // 2. 兜底1:裁剪临时目录失败,切换到裁剪兜底目录 + if (isDirActuallyWritable(fCropFallbackDir)) { + try { + cropTempFile = new File(fCropFallbackDir, CROP_TEMP_FILE_NAME); + clearOldFile(cropTempFile, "裁剪临时文件(兜底目录)"); + cropTempFile.createNewFile(); + setFilePermissions(cropTempFile); + LogUtils.w(TAG, "【裁剪路径】裁剪临时目录失败,切换到兜底目录创建成功:" + cropTempFile.getAbsolutePath()); + return cropTempFile; + } catch (IOException e) { + LogUtils.e(TAG, "【裁剪路径】裁剪兜底目录创建文件失败:" + e.getMessage(), e); + } + } + + // 3. 终极兜底:应用内部缓存目录(确保不崩溃) + File cacheDir = mContext.getCacheDir(); + if (isDirActuallyWritable(cacheDir)) { + try { + cropTempFile = new File(cacheDir, CROP_TEMP_FILE_NAME); + clearOldFile(cropTempFile, "裁剪临时文件(终极兜底)"); + cropTempFile.createNewFile(); + setFilePermissions(cropTempFile); + LogUtils.w(TAG, "【裁剪路径】应用外部目录全部失败,终极兜底到缓存目录:" + cropTempFile.getAbsolutePath()); + return cropTempFile; + } catch (IOException e) { + LogUtils.e(TAG, "【裁剪路径】终极兜底目录创建文件失败:" + e.getMessage(), e); + ToastUtils.show("裁剪路径创建失败,请重启应用"); + } + } + + // 极端情况:所有目录均失败(返回null,上层需处理) + LogUtils.e(TAG, "【裁剪路径】所有目录均无法创建裁剪文件,系统裁剪功能不可用"); + return null; } /** @@ -97,7 +194,7 @@ public class BackgroundSourceUtils { if (currentBackgroundBean == null) { currentBackgroundBean = new BackgroundBean(); BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); - LogUtils.d(TAG, "正式背景Bean不存在,创建默认Bean"); + LogUtils.d(TAG, "【配置管理】正式背景Bean不存在,创建默认Bean"); } // 加载预览Bean @@ -105,7 +202,7 @@ public class BackgroundSourceUtils { if (previewBackgroundBean == null) { previewBackgroundBean = new BackgroundBean(); BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); - LogUtils.d(TAG, "预览背景Bean不存在,创建默认Bean"); + LogUtils.d(TAG, "【配置管理】预览背景Bean不存在,创建默认Bean"); } } @@ -129,7 +226,7 @@ public class BackgroundSourceUtils { public String getCurrentBackgroundFilePath() { loadSettings(); // 加载最新配置,避免数据滞后 File file = new File(fBackgroundSourceDir, currentBackgroundBean.getBackgroundFileName()); - LogUtils.d(TAG, "正式背景路径:" + file.getAbsolutePath()); + LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath()); return file.getAbsolutePath(); } @@ -139,7 +236,7 @@ public class BackgroundSourceUtils { public String getPreviewBackgroundFilePath() { loadSettings(); // 加载最新配置,避免数据滞后 File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundFileName()); - LogUtils.d(TAG, "预览背景路径:" + file.getAbsolutePath()); + LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath()); return file.getAbsolutePath(); } @@ -149,7 +246,7 @@ public class BackgroundSourceUtils { public String getPreviewBackgroundScaledCompressFilePath() { loadSettings(); // 加载最新配置,避免数据滞后 File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundScaledCompressFileName()); - LogUtils.d(TAG, "预览压缩背景路径:" + file.getAbsolutePath()); + LogUtils.d(TAG, "【路径管理】预览压缩背景路径:" + file.getAbsolutePath()); return file.getAbsolutePath(); } @@ -159,7 +256,7 @@ public class BackgroundSourceUtils { public void saveSettings() { BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); - LogUtils.d(TAG, "配置保存成功:正式Bean=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览Bean=" + previewBackgroundBeanFile.getAbsolutePath()); + LogUtils.d(TAG, "【配置管理】配置保存成功:正式Bean=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览Bean=" + previewBackgroundBeanFile.getAbsolutePath()); } /** @@ -169,6 +266,27 @@ public class BackgroundSourceUtils { return fBackgroundSourceDir.getAbsolutePath(); } + /** + * 获取裁剪临时文件(对外提供,Activity中用于传递给系统裁剪应用) + */ + public File getCropTempFile() { + return cropTempFile; + } + + /** + * 获取裁剪结果文件(对外提供,Activity中用于获取裁剪后的图片) + */ + public File getCropResultFile() { + return cropResultFile; + } + + /** + * 获取FileProvider授权Authority(多包名兼容,对外提供给Activity) + */ + public String getFileProviderAuthority() { + return FILE_PROVIDER_AUTHORITY; + } + /** * 保存图片到预览Bean(核心修复:解决文件名覆盖+路径无效问题) * @param sourceFile 源图片文件(非空,必须存在) @@ -178,15 +296,14 @@ public class BackgroundSourceUtils { public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) { // 校验源文件合法性 if (sourceFile == null || !sourceFile.exists() || !sourceFile.isFile()) { - LogUtils.e(TAG, "源文件无效:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); + LogUtils.e(TAG, "【文件管理】源文件无效:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); ToastUtils.show("源图片文件无效"); return previewBackgroundBean; } // 确保背景目录存在(防止首次使用时目录未创建) if (!fBackgroundSourceDir.exists()) { - fBackgroundSourceDir.mkdirs(); - LogUtils.d(TAG, "背景目录不存在,自动创建:" + fBackgroundSourceDir.getAbsolutePath()); + createDirWithPermission(fBackgroundSourceDir, "背景图片目录(saveFileToPreviewBean)"); } // 生成唯一文件名(基于源文件后缀,避免重复) @@ -196,7 +313,7 @@ public class BackgroundSourceUtils { // 复制源文件到预览目录(确保图片实际保存成功) boolean copySuccess = FileUtils.copyFile(sourceFile, previewBackgroundFile); if (!copySuccess) { - LogUtils.e(TAG, "图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + " → " + previewBackgroundFile.getAbsolutePath()); + LogUtils.e(TAG, "【文件管理】图片复制到预览目录失败:" + sourceFile.getAbsolutePath() + " → " + previewBackgroundFile.getAbsolutePath()); ToastUtils.show("预览图片保存失败"); return previewBackgroundBean; } @@ -206,12 +323,13 @@ public class BackgroundSourceUtils { previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName()); // 正确赋值:唯一文件名 previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_" + previewBackgroundFile.getName()); // 压缩文件名(前缀标识) previewBackgroundBean.setBackgroundFileInfo(fileInfo); // 正确赋值:附加信息(Uri) - previewBackgroundBean.setIsUseBackgroundFile(true); // 标记使用背景图,确保BackgroundView加载 + previewBackgroundBean.setIsUseBackgroundFile(true); // 标记使用背景图 + previewBackgroundBean.setIsUseScaledCompress(true); // 启用压缩图,提升预览加载速度 previewBackgroundBean.setBackgroundWidth(100); // 默认宽高比1:1(可根据需求调整) previewBackgroundBean.setBackgroundHeight(100); saveSettings(); // 持久化保存预览Bean - LogUtils.d(TAG, "预览图片保存成功:" + previewBackgroundFile.getAbsolutePath()); + LogUtils.d(TAG, "【文件管理】预览图片保存成功:" + previewBackgroundFile.getAbsolutePath()); ToastUtils.show("预览图片加载成功"); return previewBackgroundBean; } @@ -232,7 +350,7 @@ public class BackgroundSourceUtils { currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor()); saveSettings(); // 持久化保存正式Bean - LogUtils.d(TAG, "预览背景提交成功:正式背景更新为预览背景"); + LogUtils.d(TAG, "【配置管理】预览背景提交成功:正式背景更新为预览背景"); ToastUtils.show("背景图片应用成功"); } @@ -252,7 +370,238 @@ public class BackgroundSourceUtils { previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); saveSettings(); // 持久化保存预览Bean - LogUtils.d(TAG, "正式背景同步到预览:预览背景更新为当前正式背景"); + LogUtils.d(TAG, "【配置管理】正式背景同步到预览:预览背景更新为当前正式背景"); + } + + /** + * 工具方法:创建目录并设置权限(确保目录可读写,适配Android14+) + * @param dir 要创建的目录 + * @param dirDesc 目录描述(用于日志打印) + */ + private void createDirWithPermission(File dir, String dirDesc) { + if (dir == null) { + LogUtils.e(TAG, "【文件管理】创建目录失败:目录对象为null(描述:" + dirDesc + ")"); + return; + } + if (!dir.exists()) { + boolean mkdirsSuccess = dir.mkdirs(); + LogUtils.d(TAG, "【文件管理】" + dirDesc + "创建结果:" + (mkdirsSuccess ? "成功" : "失败") + ",路径:" + dir.getAbsolutePath()); + if (!mkdirsSuccess) { + LogUtils.w(TAG, "【文件管理】" + dirDesc + "创建失败,尝试创建父目录"); + File parentDir = dir.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + dir.mkdir(); + } + } + } + // 强制设置目录权限(递归设置,确保所有层级可读写) + setDirPermissionsRecursively(dir); + } + + /** + * 工具方法:递归设置目录及子目录/文件的读写权限(适配Android全版本) + * @param dir 要设置权限的目录 + */ + private void setDirPermissionsRecursively(File dir) { + if (dir == null || !dir.exists()) { + String dirPath = (dir != null) ? dir.getAbsolutePath() : "null"; + LogUtils.d(TAG, "【权限管理】目录无效,无需设置权限:" + dirPath); + return; + } + try { + // 设置目录权限(允许所有用户读写,系统裁剪应用必需) + dir.setReadable(true, false); + dir.setWritable(true, false); + dir.setExecutable(false, false); + + LogUtils.d(TAG, "【权限管理】目录权限设置完成:路径=" + dir.getAbsolutePath() + ",可写=" + dir.canWrite() + ",可读=" + dir.canRead()); + + // 递归处理子目录和文件 + File[] files = dir.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + if (file.isDirectory()) { + setDirPermissionsRecursively(file); + } else { + // 设置文件权限(与目录一致) + file.setReadable(true, false); + file.setWritable(true, false); + file.setExecutable(false, false); + // 裁剪相关文件单独打印日志 + if (file.getName().contains(CROP_TEMP_FILE_NAME) || file.getName().contains(CROP_RESULT_FILE_NAME)) { + LogUtils.d(TAG, "【权限管理】裁剪文件权限设置:文件名=" + file.getName() + ",可写=" + file.canWrite()); + } + } + } + } + } catch (SecurityException e) { + LogUtils.e(TAG, "【权限管理】设置目录权限失败(系统禁止):" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e); + } catch (Exception e) { + LogUtils.e(TAG, "【权限管理】设置目录权限异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e); + } + } + + /** + * 工具方法:设置单个文件权限(确保系统裁剪应用可读写) + * 【关键调整】修改为public,适配BackgroundSettingsActivity的外部调用 + * @param file 要设置权限的文件 + */ + public void setFilePermissions(File file) { + if (file == null || !file.exists()) { + LogUtils.d(TAG, "【权限管理】文件无效,无需设置权限:" + (file != null ? file.getAbsolutePath() : "null")); + return; + } + try { + // 核心:允许所有用户读写(系统裁剪应用非本应用进程,需开放权限) + file.setReadable(true, false); + file.setWritable(true, false); + file.setExecutable(false, false); + LogUtils.d(TAG, "【权限管理】文件权限设置完成:路径=" + file.getAbsolutePath() + ",可写=" + file.canWrite() + ",可读=" + file.canRead()); + } catch (Exception e) { + LogUtils.e(TAG, "【权限管理】设置文件权限失败:" + file.getAbsolutePath() + ",错误:" + e.getMessage(), e); + } + } + + /** + * 工具方法:清理旧文件(避免文件锁定/残留) + * @param file 要清理的文件 + * @param fileDesc 文件描述(用于日志打印) + */ + /** + * 工具方法:清理旧文件(避免文件锁定/残留)【内部私有,不对外暴露】 + * @param file 要清理的文件 + * @param fileDesc 文件描述(用于日志打印) + */ + private void clearOldFile(File file, String fileDesc) { + if (file == null) { + return; + } + if (file.exists()) { + boolean deleteSuccess = file.delete(); + LogUtils.d(TAG, "【文件管理】清理" + fileDesc + ":" + (deleteSuccess ? "成功" : "失败") + ",路径:" + file.getAbsolutePath()); + // 若删除失败,标记为退出时删除(兼容文件锁定场景) + if (!deleteSuccess) { + file.deleteOnExit(); + LogUtils.w(TAG, "【文件管理】" + fileDesc + "删除失败,标记为退出时自动删除"); + } + } + } + + /** + * 工具方法:验证目录实际写入能力(解决Android14+ canWrite()假阳性问题) + * 原理:通过创建临时空文件并删除,验证目录是否真的可写 + * @param dir 要验证的目录 + * @return true=实际可写,false=实际不可写 + */ + private boolean isDirActuallyWritable(File dir) { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + return false; + } + // 创建临时空文件(随机文件名,避免冲突) + File testFile = new File(dir, "test_write_" + System.currentTimeMillis() + ".tmp"); + try { + boolean createSuccess = testFile.createNewFile(); + if (createSuccess) { + boolean canWrite = testFile.canWrite(); + testFile.delete(); // 删除临时文件,不占用空间 + LogUtils.d(TAG, "【权限校验】目录实际写入校验:" + dir.getAbsolutePath() + ",结果=" + (canWrite ? "通过" : "失败")); + return canWrite; + } else { + LogUtils.d(TAG, "【权限校验】目录实际写入校验失败:" + dir.getAbsolutePath() + ",创建临时文件失败"); + return false; + } + } catch (IOException e) { + LogUtils.e(TAG, "【权限校验】目录实际写入校验异常:" + dir.getAbsolutePath() + ",错误:" + e.getMessage(), e); + return false; + } + } + + /** + * 工具方法:复制文件(适配大文件,避免OOM) + * 【关键优化】兼容源文件为空的场景(适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用) + * @param source 源文件(可为空,为空时仅创建目标目录) + * @param target 目标文件/目录(若源文件为空,target视为目录并创建) + * @return true=复制/创建成功,false=失败 + */ + public boolean copyFile(File source, File target) { + // 场景1:源文件为空 → 仅创建目标目录(适配Activity的目录创建调用) + if (source == null || (source.exists() && source.length() <= 0)) { + if (target == null) { + LogUtils.e(TAG, "【文件管理】目录创建失败:目标目录对象为null"); + return false; + } + // 若target是文件,取其父目录;若本身是目录,直接创建 + File targetDir = target.isFile() ? target.getParentFile() : target; + createDirWithPermission(targetDir, "空源文件场景-目录创建"); + LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath()); + return true; + } + + // 场景2:正常文件复制(源文件非空且存在) + if (!source.exists() || target == null) { + LogUtils.e(TAG, "【文件管理】文件复制失败:源文件无效或目标文件为空"); + return false; + } + // 确保目标目录存在 + File targetDir = target.getParentFile(); + if (!targetDir.exists()) { + createDirWithPermission(targetDir, "文件复制目标目录"); + } + // 调用FileUtils复制(若项目中已有该方法,可直接复用) + return FileUtils.copyFile(source, target); + } + + /** + * 工具方法:清理裁剪相关临时文件(对外提供,Activity退出时调用) + */ + public void clearCropTempFiles() { + clearOldFile(cropTempFile, "裁剪临时文件"); + clearOldFile(cropResultFile, "裁剪结果文件"); + // 清理裁剪目录下的其他临时文件 + if (fCropTempDir.exists()) { + File[] files = fCropTempDir.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + if (file.isFile()) { + file.delete(); + } + } + } + } + LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成"); + } + + /** + * 对外接口:清理指定旧文件(适配BackgroundSettingsActivity调用) + * @param file 要清理的文件 + * @param fileDesc 文件描述(用于日志打印) + */ + public void clearOldFileByExternal(File file, String fileDesc) { + clearOldFile(file, fileDesc); // 调用内部private方法,复用逻辑 + } + + /** + * 工具方法:获取目录类型描述(用于日志调试,明确目录类型) + * @param dir 目标目录 + * @return 目录类型描述 + */ + public String getDirTypeDesc(File dir) { + if (dir == null) { + return "未知目录(null)"; + } + String dirPath = dir.getAbsolutePath(); + String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : ""; + String cachePath = mContext.getCacheDir().getAbsolutePath(); + + if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) { + return "应用私有外部目录(getExternalFilesDir(),系统裁剪可读写)"; + } else if (dirPath.contains(cachePath)) { + return "应用内部缓存目录(getCacheDir(),兜底目录)"; + } else { + return "外部存储目录(非应用私有,权限受限)"; + } } } +