@@ -17,22 +17,18 @@ import java.io.OutputStream;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 12:07:20
* @Describe 背景图片工具集(全量文件管理+裁剪FileProvider路径适配, 线程安全+数据流转正常 )
* 核心能力:
* 1. 统一管理所有文件路径(背景图/裁剪临时文件/压缩图)
* 2. 为系统裁剪应用创建可读写的FileProvider路径( 适配Android14+ MIUI)
* 3. 替代BackgroundSettingsActivity的所有文件操作逻辑
* @Describe 背景图片工具集(调整版:图片存储→/Pictures/PowerBell, JSON→应用外置存储 )
*/
public class BackgroundSourceUtils {
public static final String TAG = " BackgroundSourceUtils " ;
// 裁剪相关常量(统一定义,避免硬编码)
private static final String CROP_TEMP_DIR_NAME = " CropTemp " ; // 裁剪临时 目录(FileProvider适配 )
private static final String CROP_TEMP_DIR_NAME = " cache " ; // 裁剪缓存 目录(基础目录下 )
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 CROP_INNER_DIR_NAME = " CropInner " ; // 优先裁剪目录( BackgroundSource下)
private static final String FILE_PROVIDER_AUTHORITY = BuildConfig . APPLICATION_ID + " .fileprovider " ; // 多包名兼容
// 图片操作基础目录(核心调整:系统公共图片目录)
private static final String PICTURE_BASE_DIR = Environment . getExternalStoragePublicDirectory ( Environment . DIRECTORY_PICTURES ) + File . separator + " PowerBell " ;
// 1. 静态实例加volatile, 禁止指令重排, 保证可见性( 双重校验锁单例核心)
private static volatile BackgroundSourceUtils sInstance ;
@@ -42,16 +38,17 @@ public class BackgroundSourceUtils {
private File previewBackgroundBeanFile ;
private BackgroundBean previewBackgroundBean ;
// 2. 统一文件目录(全量文件管理, 替代Activity中的目录变量 )
// 2. 统一文件目录(分两类: 图片目录→系统公共目录, JSON目录→应用外置存储 )
// 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/)
private File fPictureBaseDir ; // 图片基础目录
private File fPictureCacheDir ; // 裁剪缓存目录(基础目录下/cache)
private File fBackgroundSourceDir ; // 图片存储目录(基础目录下,存储正式/预览图)
// JSON配置目录( 原应用外置存储目录, 不改变)
private File fUtilsDir ; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils)
private File fModelDir ; // 模型文件目录(存储BackgroundBean的 JSON文件 )
private File fBackgroundSourceDir ; // 背景图片源目录(存储正式/预览图片 )
private File fC ropTempDir ; // 裁剪临时目录( FileProvider适配路径, 系统裁剪应用可读写 )
private File fC ropFallbackDir ; // 裁剪兜底目录(应用私有外部目录失败时使用 )
private File fCropInnerDir ; // 新增: 优先裁剪目录( BackgroundSource下, 权限更可控)
private File cropTempFile ; // 裁剪临时文件(系统裁剪应用写入目标)
private File cropInnerTempFile ; // 新增: 优先裁剪临时文件( CropInner目录下)
private File cropResultFile ; // 裁剪结果文件(裁剪后保存的最终文件)
private File fModelDir ; // 模型文件目录( 存储JSON配置 )
// 裁剪文件( 统一放入图片基础目录下的cache )
private File c ropTempFile ; // 裁剪临时文件( fPictureCacheDir下 )
private File c ropResultFile ; // 裁剪结果文件( fBackgroundSourceDir下 )
// 3. 私有构造器(加防反射逻辑+初始化所有目录/文件)
private BackgroundSourceUtils ( Context context ) {
@@ -61,21 +58,19 @@ public class BackgroundSourceUtils {
}
// 上下文用Application Context, 避免Activity内存泄漏
this . mContext = context . getApplicationContext ( ) ;
// 初始化所有 目录(文件管理核心:统一创建+权限设置 )
initAll Dirs ( ) ;
// 初始化目录(分图片目录+JSON目录 )
initPicture Dirs ( ) ; // 初始化图片操作目录(系统公共目录)
initJsonDirs ( ) ; // 初始化JSON配置目录( 应用外置存储)
// 初始化所有文件(裁剪临时文件/结果文件等)
initAllFiles ( ) ;
// 加载配置(正式/预览Bean)
// 加载配置(正式/预览Bean, JSON目录下)
loadSettings ( ) ;
}
// 4. 双重校验锁单例( 线程安全, 高效, 支持多线程并发调用, Java7语法兼容)
public static BackgroundSourceUtils getInstance ( Context context ) {
// 第一重校验:避免每次调用都加锁(提升效率)
if ( sInstance = = null ) {
// 同步锁:保证同一时刻只有一个线程进入创建逻辑
synchronized ( BackgroundSourceUtils . class ) {
// 第二重校验:防止多线程并发时重复创建(核心)
if ( sInstance = = null ) {
sInstance = new BackgroundSourceUtils ( context ) ;
}
@@ -85,113 +80,101 @@ public class BackgroundSourceUtils {
}
/**
* 初始化所有文件目录( 修改: 优先初始化CropInner目录, 确保权限可控 )
* 包含:工具类根目录、模型目录、背景图目录、裁剪临时目录、裁剪兜底目录、优先裁剪目录
* 初始化图片操作目录(核心调整:系统公共图片目录 /Pictures/PowerBell/ )
*/
private void initAll Dirs ( ) {
// 1. 工具类根目录(应用外部存储:/Android/data/包名/files/BackgroundSourceUtils)
fUtils Dir = mContext . getExternalFilesDir ( TAG ) ;
if ( fUtilsDir = = null ) {
LogUtils . e ( TAG , " 【文件管理】应用外部存储不可用,切换到应用内部缓存目录 " ) ;
fUtilsDir = mContext . getCacheDir ( ) ; // 极端兜底:应用内部缓存目录
private void initPicture Dirs ( ) {
// 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell
fPictureBase Dir = new File ( PICTURE_BASE_DIR ) ;
// 2. 图片存储目录:基础目录下(存储正式/预览图片)
fBackgroundSourceDir = new File ( fPictureBaseDir , " BackgroundSource " ) ;
// 3. 裁剪缓存目录:基础目录下/cache( 所有裁剪操作在此目录)
fPictureCacheDir = new File ( fPictureBaseDir , CROP_TEMP_DIR_NAME ) ;
// 4. 递归创建目录(系统公共目录需强制授权,确保创建成功)
createDirWithPermission ( fPictureBaseDir , " 图片基础目录(/Pictures/PowerBell) " ) ;
createDirWithPermission ( fBackgroundSourceDir , " 图片存储目录(基础目录下) " ) ;
createDirWithPermission ( fPictureCacheDir , " 裁剪缓存目录(基础目录/cache) " ) ;
LogUtils . d ( TAG , " 【图片目录初始化】完成:基础目录= " + fPictureBaseDir . getAbsolutePath ( ) + " ,裁剪缓存目录= " + fPictureCacheDir . getAbsolutePath ( ) ) ;
}
// 2. 子目录初始化( 按功能划分, 新增优先裁剪目录CropInner)
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 ) ; // 裁剪兜底目录
fCropInner Dir = new File ( fBackgroundSourceDir , CROP_INNER_DIR_NAME ) ; // 优先裁剪目录( BackgroundSource下)
/**
* 初始化JSON配置目录( 保留原逻辑: 应用外置存储)
*/
private void initJsonDirs ( ) {
// 1. 工具类根目录(应用外置存储)
fUtils Dir = mContext . getExternalFilesDir ( TAG ) ;
if ( fUtilsDir = = null ) {
LogUtils . e ( TAG , " 【JSON目录】应用外置存储不可用, 切换到应用内部缓存目录 " ) ;
fUtilsDir = mContext . getCacheDir ( ) ;
}
// 2. 模型文件目录( 存储JSON配置)
fModelDir = new File ( fUtilsDir , " ModelDir " ) ;
createDirWithPermission ( fModelDir , " JSON配置目录( 应用外置存储) " ) ;
// 3. 递归创建所有目录( 修改: 优先创建CropInner目录, 确保权限初始化)
createDirWithPermission ( fModelDir , " 模型文件目录 " ) ;
createDirWithPermission ( fBackgroundSourceDir , " 背景图片目录 " ) ;
createDirWithPermission ( fCropInnerDir , " 优先裁剪目录( BackgroundSource下) " ) ; // 优先创建
createDirWithPermission ( fCropTempDir , " 裁剪临时目录( FileProvider适配) " ) ;
createDirWithPermission ( fCropFallbackDir , " 裁剪兜底目录 " ) ;
// 4. 初始化Bean文件对象( 存储JSON配置)
// 3. 初始化JSON文件对象
currentBackgroundBeanFile = new File ( fModelDir , " currentBackgroundBean.json " ) ;
previewBackgroundBeanFile = new File ( fModelDir , " previewBackgroundBean.json " ) ;
LogUtils . d ( TAG , " 【文件管理】所有 目录初始化完成:根 目录= " + fUtilsDir . getAbsolutePath ( ) + " ,优先裁剪目录= " + fCropInner Dir . getAbsolutePath ( ) ) ;
LogUtils . d ( TAG , " 【JSON 目录初始化】 完成:目录= " + fModel Dir . getAbsolutePath ( ) ) ;
}
/**
* 初始化所有文件(修改:新增优先裁剪文件初始化 )
* 包含:优先裁剪临时文件、原裁剪临时文件、裁剪结果文件
* 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录 )
*/
private void initAllFiles ( ) {
// 1. 新增:优先 裁剪临时文件(BackgroundSource/CropInner下, FileProvider已配置 )
cropInner TempFile = new File ( fCropInner Dir , CROP_TEMP_FILE_NAME ) ;
// 2. 原 裁剪临时 文件(兼容旧逻辑 )
cropTempFile = new File ( fCropTempDir , CROP_TEMP_FILE_NAME ) ;
// 3. 裁剪结果文件(裁剪后保存的最终文件,存入背景图片目录)
// 1. 裁剪临时文件(图片基础目录/cache下, 系统裁剪可读写 )
cropTempFile = new File ( fPictureCache Dir , CROP_TEMP_FILE_NAME ) ;
// 2. 裁剪结果 文件(图片存储目录下,最终保存的裁剪图 )
cropResultFile = new File ( fBackgroundSourceDir , CROP_RESULT_FILE_NAME ) ;
// 4 . 初始化时清理旧文件(避免文件锁定/权限残留)
clearOldFile ( cropInner TempFile , " 旧优先 裁剪临时文件 " ) ; // 清理优先裁剪文件
clearOldFile ( cropTemp File , " 旧裁剪临时文件 " ) ;
clearOldFile ( cropResultFile , " 旧裁剪结果文件 " ) ;
// 3 . 初始化时清理旧文件(避免文件锁定/权限残留)
clearOldFile ( cropTempFile , " 旧裁剪临时文件( /Pictures/PowerBell/cache) " ) ;
clearOldFile ( cropResult File , " 旧裁剪结果文件(/Pictures/PowerBell/BackgroundSource) " ) ;
LogUtils . d ( TAG , " 【文件管理】所有文件 初始化完成:优先 裁剪临时文件= " + cropInner TempFile . getAbsolutePath ( ) + " ,裁剪结果文件= " + cropResultFile . getAbsolutePath ( ) ) ;
LogUtils . d ( TAG , " 【文件初始化】 完成:裁剪临时文件= " + cropTempFile . getAbsolutePath ( ) + " ,裁剪结果文件= " + cropResultFile . getAbsolutePath ( ) ) ;
}
/**
* 核心函数: 为系统裁剪应用创建可读写的FileProvider路径( 修改: 优先使用CropInner目录 )
* 适配: Android14+、MIUI, 解决Permission denied+裁剪文件为0字节问题
* @return 裁剪临时文件( File) , 系统裁剪应用可读写, 路径已适配FileProvider
* 核心函数: 为系统裁剪应用创建可读写的FileProvider路径( 适配Android14+ MIUI )
* 裁剪文件统一放入 /Pictures/PowerBell/cache/,确保系统裁剪工具可读写
*/
public File createCropFileProviderPath ( ) {
LogUtils . d ( TAG , " 【裁剪路径】createCropFileProviderPath 触发,创建系统裁剪可读写路径 " ) ;
// 1. 优先使用BackgroundSource下的CropInner目录( 核心修改: 权限可控, FileProvider已配置 )
if ( fCropInner Dir ! = null & & fCropInner Dir . exists ( ) & & isDirActuallyWritable ( fCropInner Dir ) ) {
// 优先使用图片基础目录下的cache目录( 核心: 系统公共目录, 权限更友好 )
if ( fPictureCache Dir ! = null & & fPictureCache Dir . exists ( ) & & isDirActuallyWritable ( fPictureCache Dir ) ) {
try {
// 重新初始化优先裁剪临时文件(先删后建 ,避免文件 锁定)
clearOldFile ( cropInner TempFile , " 优先 裁剪临时文件(重新初始化)" ) ;
cropInnerTempFile . createNewFile ( ) ;
// 强制设置文件权限(系统裁剪应用必需:允许所有用户读写)
setFilePermissions ( cropInnerTempFile ) ;
// 关键: 将当前裁剪文件指向优先裁剪文件( 上层Activity直接使用)
cropTempFile = cropInner TempFile ;
LogUtils . d ( TAG , " 【裁剪路径】系统裁剪可读写路径创建成功(优先裁剪目录): " + cropTempFile . getAbsolutePath ( ) ) ;
// 先清理旧文件 ,避免锁定
clearOldFile ( cropTempFile , " 裁剪临时文件(重新初始化) " ) ;
// 创建新的裁剪临时文件
cropTempFile . createNewFile ( ) ;
// 强制设置权限(系统裁剪应用必需:允许所有用户读写)
setFilePermissions ( cropTempFile ) ;
LogUtils . d ( TAG , " 【裁剪路径】创建成功(/Pictures/PowerBell/cache) : " + cropTempFile . getAbsolutePath ( ) ) ;
return cropTempFile ;
} catch ( IOException e ) {
LogUtils . e ( TAG , " 【裁剪路径】优先裁剪 目录创建文件失败: " + e . getMessage ( ) , e ) ;
LogUtils . e ( TAG , " 【裁剪路径】cache 目录创建文件失败: " + e . getMessage ( ) , e ) ;
}
} else {
LogUtils . w ( TAG , " 【裁剪路径】优先裁剪目录不可用(不存在或无权限),切换到备用目录 " ) ;
LogUtils . w ( TAG , " 【裁剪路径】cache目录不可用, 切换到图片存储目录兜底 " ) ;
}
// 2. 备用: 尝试裁剪临时目录( FileProvider适配路径 )
if ( isDirActuallyWritable ( fCropTemp Dir ) ) {
// 兜底:使用图片存储目录(极端情况 )
if ( isDirActuallyWritable ( fBackgroundSource Dir ) ) {
try {
clearOldFile ( cropTempFile , " 裁剪临时文件(重新初始化) " ) ;
cropTempFile . createNewFile ( ) ;
setFilePermissions ( cropTempFile ) ;
LogUtils . d ( TAG , " 【裁剪路径】系统裁剪可读写路径创建成功(裁剪临时目录): " + cropTempFile . getAbsolutePath ( ) ) ;
return cropTempFile ;
} catch ( IOException e ) {
LogUtils . e ( TAG , " 【裁剪路径】裁剪临时目录创建文件失败: " + e . getMessage ( ) , e ) ;
}
}
// 3. 兜底1: 裁剪兜底目录
if ( isDirActuallyWritable ( fCropFallbackDir ) ) {
try {
cropTempFile = new File ( fCropFallbackDir , CROP_TEMP_FILE_NAME ) ;
cropTempFile = new File ( fBackgroundSourceDir , CROP_TEMP_FILE_NAME ) ;
clearOldFile ( cropTempFile , " 裁剪临时文件(兜底目录) " ) ;
cropTempFile . createNewFile ( ) ;
setFilePermissions ( cropTempFile ) ;
LogUtils . w ( TAG , " 【裁剪路径】裁剪临时目录失败,切换到兜底 目录创建成功: " + cropTempFile . getAbsolutePath ( ) ) ;
LogUtils . w ( TAG , " 【裁剪路径】切换到图片存储 目录创建成功: " + cropTempFile . getAbsolutePath ( ) ) ;
return cropTempFile ;
} catch ( IOException e ) {
LogUtils . e ( TAG , " 【裁剪路径】裁剪兜底 目录创建文件失败: " + e . getMessage ( ) , e ) ;
LogUtils . e ( TAG , " 【裁剪路径】图片存储 目录创建文件失败: " + e . getMessage ( ) , e ) ;
}
}
// 4. 终极兜底:应用内部缓存目录(提示用户权限问题)
// 终极兜底:应用内部缓存目录(提示用户权限问题)
File cacheDir = mContext . getCacheDir ( ) ;
if ( isDirActuallyWritable ( cacheDir ) ) {
try {
@@ -199,23 +182,22 @@ public class BackgroundSourceUtils {
clearOldFile ( cropTempFile , " 裁剪临时文件(终极兜底) " ) ;
cropTempFile . createNewFile ( ) ;
setFilePermissions ( cropTempFile ) ;
LogUtils . w ( TAG , " 【裁剪路径】应用外部目录全部 失败,终极 兜底到缓存目录 ( MIUI可能裁剪失败) : " + cropTempFile . getAbsolutePath ( ) ) ;
LogUtils . w ( TAG , " 【裁剪路径】系统公共目录 失败,兜底到应用 缓存( MIUI可能裁剪失败) : " + cropTempFile . getAbsolutePath ( ) ) ;
ToastUtils . show ( " 存储权限受限,建议授予「所有文件访问权限」以确保裁剪正常 " ) ;
return cropTempFile ;
} catch ( IOException e ) {
LogUtils . e ( TAG , " 【裁剪路径】终极兜底目录创建文件 失败: " + e . getMessage ( ) , e ) ;
LogUtils . e ( TAG , " 【裁剪路径】终极兜底目录创建失败: " + e . getMessage ( ) , e ) ;
ToastUtils . show ( " 裁剪路径创建失败,请重启应用并授予存储权限 " ) ;
}
}
// 极端情况:所有目录均失败( 返回null, 上层需处理)
LogUtils . e ( TAG , " 【裁剪路径】所有目录均无法创建裁剪文件,系统 裁剪功能不可用 " ) ;
// 极端情况:所有目录均失败
LogUtils . e ( TAG , " 【裁剪路径】所有目录均无法创建裁剪文件,裁剪功能不可用 " ) ;
return null ;
}
/**
* 加载背景图片配置数据(正式/预览Bean )
* 从JSON文件读取, 若文件不存在则创建默认Bean并保存
* 加载背景图片配置数据(JSON文件仍在应用外置存储, 不改变 )
*/
void loadSettings ( ) {
// 加载正式Bean
@@ -235,52 +217,64 @@ public class BackgroundSourceUtils {
}
}
/**
* 获取正式背景Bean( 对外提供, 用于修改正式配置)
*/
// ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------
public BackgroundBean getCurrentBackgroundBean ( ) {
return currentBackgroundBean ;
}
/**
* 获取预览背景Bean( 对外提供, 用于修改预览配置)
*/
public BackgroundBean getPreviewBackgroundBean ( ) {
return previewBackgroundBean ;
}
/**
* 获取正式背景图片路径(拼接:背景目录+正式Bean中的文件名 )
* 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验 )
*/
public String getCurrentBackgroundFilePath ( ) {
loadSettings( ) ; // 加载最新配置,避免数据滞后
File file = new File ( fBackgroundSourceDir , currentBackgroundBean . getBackgroundFileName ( ) ) ;
// 移除: loadSettings(); // 关键修复: 避免每次调用都重新加载Bean, 导致字段被重置为空
String fileName = currentBackgroundBean . getBackgroundFileName ( ) ;
// 强化校验:若文件名为空,返回空路径(避免拼接目录路径)
if ( TextUtils . isEmpty ( fileName ) ) {
LogUtils . e ( TAG , " 【路径管理】正式背景文件名为空,返回空路径 " ) ;
return " " ;
}
File file = new File ( fBackgroundSourceDir , fileName ) ;
LogUtils . d ( TAG , " 【路径管理】正式背景路径: " + file . getAbsolutePath ( ) ) ;
return file . getAbsolutePath ( ) ;
}
/**
* 获取预览背景图片路径(拼接:背景目录+预览Bean中的文件名 )
* 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验 )
*/
public String getPreviewBackgroundFilePath ( ) {
loadSettings( ) ; // 加载最新配置,避免数据滞后
File file = new File ( fBackgroundSourceDir , previewBackgroundBean . getBackgroundFileName ( ) ) ;
// 移除: loadSettings(); // 关键修复: 避免每次调用都重新加载Bean, 导致字段被重置为空
String fileName = previewBackgroundBean . getBackgroundFileName ( ) ;
// 强化校验:若文件名为空,返回空路径(避免拼接目录路径)
if ( TextUtils . isEmpty ( fileName ) ) {
LogUtils . e ( TAG , " 【路径管理】预览背景文件名为空,返回空路径 " ) ;
return " " ;
}
File file = new File ( fBackgroundSourceDir , fileName ) ;
LogUtils . d ( TAG , " 【路径管理】预览背景路径: " + file . getAbsolutePath ( ) ) ;
return file . getAbsolutePath ( ) ;
}
/**
* 获取预览背景压缩图片路径(拼接:背景目录+预览Bean中的压缩文件名 )
* 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验 )
*/
public String getPreviewBackgroundScaledCompressFilePath ( ) {
loadSettings( ) ; // 加载最新配置,避免数据滞后
File file = new File ( fBackgroundSourceDir , previewBackgroundBean . getBackgroundScaledCompressFileName ( ) ) ;
// 移除: loadSettings(); // 关键修复
String compressFileName = previewBackgroundBean . getBackgroundScaledCompressFileName ( ) ;
if ( TextUtils . isEmpty ( compressFileName ) ) {
LogUtils . e ( TAG , " 【路径管理】预览压缩背景文件名为空,返回空路径 " ) ;
return " " ;
}
File file = new File ( fBackgroundSourceDir , compressFileName ) ;
LogUtils . d ( TAG , " 【路径管理】预览压缩背景路径: " + file . getAbsolutePath ( ) ) ;
return file . getAbsolutePath ( ) ;
}
/**
* 保存配置(将正式/预览Bean同步到JSON文件, 持久化 存储)
* 保存配置(JSON文件仍写入应用外置 存储)
*/
public void saveSettings ( ) {
BackgroundBean . saveBeanToFile ( currentBackgroundBeanFile . getAbsolutePath ( ) , currentBackgroundBean ) ;
@@ -289,39 +283,27 @@ public class BackgroundSourceUtils {
}
/**
* 获取背景图片源 目录路径(对外提供,用于创建临时文件 )
* 获取图片基础 目录路径(对外提供: /Pictures/PowerBell/)
*/
public String getBackgroundSourceDirPath ( ) {
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 ;
}
// ------------------------------ 工具方法(适配新目录权限)------------------------------
/**
* 新增: 流复制文件(核心修复: Android14+ 共享存储权限限制适配 )
* 不依赖文件路径, 直接通过流复制, 支持读取相册私有隐藏文件, 避免Permission denied
* @param source 源文件(可为共享存储私有文件)
* @param target 目标文件(应用私有目录,确保可写)
* @return true=复制成功, false=失败
* 流复制文件(适配系统公共目录,解决 Android14+ 权限问题 )
*/
public boolean copyFileByStream ( File source , File target ) {
if ( source = = null | | ! source . exists ( ) | | ! source . isFile ( ) | | target = = null ) {
@@ -329,40 +311,46 @@ public class BackgroundSourceUtils {
return false ;
}
// 确保目标目录存在
// 确保目标目录存在(系统公共目录需强制创建,适配/Pictures/PowerBell/路径)
File targetDir = target . getParentFile ( ) ;
if ( ! targetDir . exists ( ) ) {
createDirWithPermission ( targetDir , " 流复制目标目录 " ) ;
createDirWithPermission ( targetDir , " 流复制目标目录( /Pictures/PowerBell下) " ) ;
}
FileInputStream fis = null ;
FileOutputStream fos = null ;
try {
// 打开源文件输入流(支持共享存储私有文件)
// 打开源文件输入流(支持共享存储私有文件/系统公共目录文件 )
fis = new FileInputStream ( source ) ;
// 打开目标文件输出流
// 打开目标文件输出流(适配/Pictures/PowerBell目录权限)
fos = new FileOutputStream ( target ) ;
byte [ ] buffer = new byte [ 1024 * 8 ] ; // 8KB缓冲区, 提升复制效率
int len ;
// 循环读取流并写入目标文件( Java7 普通for循环, 兼容语法)
// 循环读取流并写入目标文件( Java7 兼容语法)
while ( ( len = fis . read ( buffer ) ) ! = - 1 ) {
fos . write ( buffer , 0 , len ) ;
}
fos . flush ( ) ;
fos . getFD ( ) . sync ( ) ; // 强制同步到磁盘,确保文件写入完成( Java7 支持)
fos . getFD ( ) . sync ( ) ; // 强制同步到磁盘,避免系统公共目录缓存导致文件损坏
LogUtils . d ( TAG , " 【文件管理】流复制成功: " + source . getAbsolutePath ( ) + " → " + target . getAbsolutePath ( ) + " ,大小: " + target . length ( ) + " bytes " ) ;
// 复制成功后强制设置目标文件权限(确保后续裁剪/预览可读写)
setFilePermissions ( target ) ;
return true ;
} catch ( Exception e ) {
LogUtils . e ( TAG , " 【文件管理】流复制异常: " + e . getMessage ( ) , e ) ;
// 复制失败时删除目标文件(避免残留空文件)
LogUtils . e ( TAG , " 【文件管理】流复制异常( /Pictures/PowerBell目录) : " + e . getMessage ( ) , e ) ;
// 复制失败时删除目标文件(避免残留空文件导致后续逻辑异常 )
if ( target . exists ( ) ) {
clearOldFileByExternal ( target , " 流复制失败残留文件 " ) ;
}
// 针对系统公共目录权限异常,给出明确提示
if ( e instanceof SecurityException | | e . getMessage ( ) . contains ( " EACCES " ) ) {
ToastUtils . show ( " 图片复制失败,请授予应用「存储权限」和「所有文件访问权限」 " ) ;
}
return false ;
} finally {
// 关闭流资源( Java7 手动关闭,避免内存泄漏,不使用 try-with-resources)
// 关闭流资源( Java7 手动关闭,避免内存泄漏,不依赖 try-with-resources)
if ( fis ! = null ) {
try {
fis . close ( ) ;
@@ -381,29 +369,29 @@ public class BackgroundSourceUtils {
}
/**
* 保存图片到预览Bean( 核心修复:替换路径复制为流复制,避免预览路径错误 )
* 保存图片到预览Bean( 图片存储到/Pictures/PowerBell/BackgroundSource, JSON仍存应用外置存储 )
* @param sourceFile 源图片文件(非空,必须存在)
* @param fileInfo 图片附加信息( 如Uri字符串, 仅作备注)
* @return 更新后的预览Bean
*/
public BackgroundBean saveFileToPreviewBean ( File sourceFile , String fileInfo ) {
// 强化校验:源文件必须存在、是文件、大小>0
// 强化校验:源文件必须存在、是文件、大小>0(避免无效文件复制)
if ( sourceFile = = null | | ! sourceFile . exists ( ) | | ! sourceFile . isFile ( ) | | sourceFile . length ( ) < = 0 ) {
LogUtils . e ( TAG , " 【文件管理】源文件无效: " + ( sourceFile ! = null ? sourceFile . getAbsolutePath ( ) : " null " ) + " ,大小: " + ( sourceFile ! = null ? sourceFile . length ( ) : 0 ) + " bytes " ) ;
ToastUtils . show ( " 源图片文件无效 " ) ;
return previewBackgroundBean ;
}
// 确保背景 目录存在(防止首次使用时目录未创建 )
// 确保图片存储 目录存在(/Pictures/PowerBell/BackgroundSource )
if ( ! fBackgroundSourceDir . exists ( ) ) {
createDirWithPermission ( fBackgroundSourceDir , " 背景 图片目录( saveFileToPreviewBean) " ) ;
createDirWithPermission ( fBackgroundSourceDir , " 图片存储 目录( saveFileToPreviewBean) " ) ;
}
// 生成唯一文件名(基于源文件后缀,避免重复)
// 生成唯一文件名(基于源文件后缀,避免重复,适配系统公共目录 )
String uniqueFileName = FileUtils . createUniqueFileName ( sourceFile ) ;
File previewBackgroundFile = new File ( fBackgroundSourceDir , uniqueFileName ) ;
// 核心修改 : 用流复制替代原FileUtils.copyFile, 解决共享存储权限问题
// 核心: 用流复制替代原FileUtils.copyFile, 适配/Pictures/PowerBell目录权限
boolean copySuccess = copyFileByStream ( sourceFile , previewBackgroundFile ) ;
if ( ! copySuccess ) {
LogUtils . e ( TAG , " 【文件管理】图片复制到预览目录失败: " + sourceFile . getAbsolutePath ( ) + " → " + previewBackgroundFile . getAbsolutePath ( ) ) ;
@@ -411,18 +399,21 @@ public class BackgroundSourceUtils {
return previewBackgroundBean ;
}
// 正确赋值预览Bean( 确保文件名非空,避免后续路径为空 )
// 正确赋值预览Bean( 确保文件名非空)
previewBackgroundBean = new BackgroundBean ( ) ;
previewBackgroundBean . setBackgroundFileName ( previewBackgroundFile . getName ( ) ) ; // 唯一文件名(非空)
previewBackgroundBean . setBackgroundScaledCompressFileName ( " ScaledCompress_ " + previewBackgroundFile . getName ( ) ) ; // 压缩文件名(前缀标识)
previewBackgroundBean . setBackgroundFileInfo ( fileInfo ) ; // 附加信息( Uri)
previewBackgroundBean . setIsUseBackgroundFile ( true ) ; // 标记使用背景图
previewBackgroundBean . setIsUseScaledCompress ( true ) ; // 启用压缩图
previewBackgroundBean . setBackgroundWidth ( 100 ) ; // 默认宽高比1:1
previewBackgroundBean . setBackgroundScaledCompressFileName ( " ScaledCompress_ " + previewBackgroundFile . getName ( ) ) ;
previewBackgroundBean . setBackgroundFileInfo ( fileInfo ) ;
previewBackgroundBean . setIsUseBackgroundFile ( true ) ;
previewBackgroundBean . setIsUseScaledCompress ( true ) ;
previewBackgroundBean . setBackgroundWidth ( 100 ) ;
previewBackgroundBean . setBackgroundHeight ( 100 ) ;
saveSettings ( ) ; // 持久化保存预览Bean
LogUtils . d ( TAG , " 【文件管理】预览图片保存成功: " + previewBackgroundFile . getAbsolutePath ( ) + " ,大小: " + previewBackgroundFile . length ( ) + " bytes " ) ;
// 关键强化: 强制保存Bean到JSON, 确保后续loadSettings()能加载到有效Bean
BackgroundBean . saveBeanToFile ( previewBackgroundBeanFile . getAbsolutePath ( ) , previewBackgroundBean ) ;
LogUtils . d ( TAG , " 【文件管理】预览Bean强制保存到JSON: " + previewBackgroundBeanFile . getAbsolutePath ( ) ) ;
LogUtils . d ( TAG , " 【文件管理】预览图片保存成功(/Pictures/PowerBell) : " + previewBackgroundFile . getAbsolutePath ( ) + " ,大小: " + previewBackgroundFile . length ( ) + " bytes " ) ;
ToastUtils . show ( " 预览图片加载成功 " ) ;
return previewBackgroundBean ;
}
@@ -442,8 +433,8 @@ public class BackgroundSourceUtils {
currentBackgroundBean . setBackgroundHeight ( previewBackgroundBean . getBackgroundHeight ( ) ) ;
currentBackgroundBean . setPixelColor ( previewBackgroundBean . getPixelColor ( ) ) ;
saveSettings ( ) ; // 持久化保存正式Bean
LogUtils . d ( TAG , " 【配置管理】预览背景提交成功:正式背景更新为预览背景 " ) ;
saveSettings ( ) ; // 持久化保存正式Bean( JSON写入应用外置存储)
LogUtils . d ( TAG , " 【配置管理】预览背景提交成功:正式背景更新为/Pictures/PowerBell下的 预览背景 " ) ;
ToastUtils . show ( " 背景图片应用成功 " ) ;
}
@@ -462,12 +453,12 @@ public class BackgroundSourceUtils {
previewBackgroundBean . setBackgroundHeight ( currentBackgroundBean . getBackgroundHeight ( ) ) ;
previewBackgroundBean . setPixelColor ( currentBackgroundBean . getPixelColor ( ) ) ;
saveSettings ( ) ; // 持久化保存预览Bean
LogUtils . d ( TAG , " 【配置管理】正式背景同步到预览:预览背景更新为当前 正式背景 " ) ;
saveSettings ( ) ; // 持久化保存预览Bean( JSON写入应用外置存储)
LogUtils . d ( TAG , " 【配置管理】正式背景同步到预览:预览背景更新为/Pictures/PowerBell下的 正式背景 " ) ;
}
/**
* 工具方法:创建目录并设置权限(确保目录可读写, 适配Android14+ )
* 工具方法:创建目录并设置权限(适配系统公共目录/Pictures/PowerBell, 确保可读写 )
* @param dir 要创建的目录
* @param dirDesc 目录描述(用于日志打印)
*/
@@ -488,12 +479,12 @@ public class BackgroundSourceUtils {
}
}
}
// 强制设置目录权限(递归设置,确保所有层级可读写)
// 强制设置目录权限(递归设置,确保系统公共目录 所有层级可读写)
setDirPermissionsRecursively ( dir ) ;
}
/**
* 工具方法:递归设置目录及子目录/文件的读写权限(适配Android全版本 )
* 工具方法:递归设置目录及子目录/文件的读写权限(适配系统公共目录/Pictures/PowerBell )
* @param dir 要设置权限的目录
*/
private void setDirPermissionsRecursively ( File dir ) {
@@ -503,7 +494,7 @@ public class BackgroundSourceUtils {
return ;
}
try {
// 设置目录权限(允许所有用户读写,系统裁剪应用必需)
// 设置目录权限(允许所有用户读写,系统裁剪应用/预览功能 必需)
dir . setReadable ( true , false ) ;
dir . setWritable ( true , false ) ;
dir . setExecutable ( false , false ) ;
@@ -518,27 +509,28 @@ public class BackgroundSourceUtils {
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 ( ) ) ;
// 裁剪/预览 相关文件单独打印日志
if ( file . getName ( ) . contains ( CROP_TEMP_FILE_NAME ) | | file . getName ( ) . contains ( CROP_RESULT_FILE_NAME ) | | file . getName ( ) . startsWith ( " ScaledCompress_ " ) ) {
LogUtils . d ( TAG , " 【权限管理】关键 文件权限设置:文件名= " + file . getName ( ) + " ,可写= " + file . canWrite ( ) ) ;
}
}
}
}
} catch ( SecurityException e ) {
LogUtils . e ( TAG , " 【权限管理】设置目录权限失败(系统禁止): " + dir . getAbsolutePath ( ) + " ,错误: " + e . getMessage ( ) , e ) ;
ToastUtils . show ( " 目录权限设置失败,请授予应用存储权限 " ) ;
} catch ( Exception e ) {
LogUtils . e ( TAG , " 【权限管理】设置目录权限异常: " + dir . getAbsolutePath ( ) + " ,错误: " + e . getMessage ( ) , e ) ;
}
}
/**
* 工具方法:设置单个文件权限(确保系统裁剪应用可读写 )
* 【关键调整】修改为 public, 适配BackgroundSettingsActivity的外部调用
* 工具方法:设置单个文件权限(确保系统裁剪应用/预览功能可读写,适配/Pictures/PowerBell目录 )
* 【关键调整】public修饰 , 适配BackgroundSettingsActivity的外部调用
* @param file 要设置权限的文件
*/
public void setFilePermissions ( File file ) {
@@ -551,14 +543,14 @@ public class BackgroundSourceUtils {
file . setReadable ( true , false ) ;
file . setWritable ( true , false ) ;
file . setExecutable ( false , false ) ;
LogUtils . d ( TAG , " 【权限管理】文件权限设置完成:路径= " + file . getAbsolutePath ( ) + " ,可写= " + file . canWrite ( ) + " ,可读= " + file . canRead ( ) ) ;
LogUtils . d ( TAG , " 【权限管理】文件权限设置完成( /Pictures/PowerBell下) :路径= " + file . getAbsolutePath ( ) + " ,可写= " + file . canWrite ( ) + " ,可读= " + file . canRead ( ) ) ;
} catch ( Exception e ) {
LogUtils . e ( TAG , " 【权限管理】设置文件权限失败: " + file . getAbsolutePath ( ) + " ,错误: " + e . getMessage ( ) , e ) ;
}
}
/**
* 工具方法:清理旧文件(避免文件锁定/残留)【内部私有,不对外暴露】
* 工具方法:清理旧文件(避免文件锁定/残留,适配系统公共目录 )【内部私有,不对外暴露】
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
@@ -567,6 +559,8 @@ public class BackgroundSourceUtils {
return ;
}
if ( file . exists ( ) ) {
// 先设置文件为可写(避免系统公共目录下文件只读导致删除失败)
file . setWritable ( true , false ) ;
boolean deleteSuccess = file . delete ( ) ;
LogUtils . d ( TAG , " 【文件管理】清理 " + fileDesc + " : " + ( deleteSuccess ? " 成功 " : " 失败 " ) + " ,路径: " + file . getAbsolutePath ( ) ) ;
// 若删除失败,标记为退出时删除(兼容文件锁定场景)
@@ -578,7 +572,7 @@ public class BackgroundSourceUtils {
}
/**
* 工具方法: 验证目录实际写入能力( 解决Android14+ canWrite()假阳性问题)
* 工具方法: 验证目录实际写入能力( 解决Android14+ canWrite()假阳性问题,适配/Pictures/PowerBell )
* 原理:通过创建临时空文件并删除,验证目录是否真的可写
* @param dir 要验证的目录
* @return true=实际可写, false=实际不可写
@@ -595,7 +589,7 @@ public class BackgroundSourceUtils {
boolean canWrite = testFile . canWrite ( ) ;
boolean canRead = testFile . canRead ( ) ;
testFile . delete ( ) ; // 删除临时文件,不占用空间
LogUtils . d ( TAG , " 【权限校验】目录实际写入校验: " + dir . getAbsolutePath ( ) + " ,创建成功= " + createSuccess + " ,可写= " + canWrite + " ,可读= " + canRead + " ,结果= " + ( canWrite ? " 通过 " : " 失败 " ) ) ;
LogUtils . d ( TAG , " 【权限校验】目录实际写入校验( /Pictures/PowerBell下) : " + dir . getAbsolutePath ( ) + " ,创建成功= " + createSuccess + " ,可写= " + canWrite + " ,可读= " + canRead + " ,结果= " + ( canWrite ? " 通过 " : " 失败 " ) ) ;
return canWrite ;
} else {
LogUtils . d ( TAG , " 【权限校验】目录实际写入校验失败: " + dir . getAbsolutePath ( ) + " , 创建临时文件失败( Permission denied) " ) ;
@@ -608,75 +602,65 @@ public class BackgroundSourceUtils {
}
/**
* 工具方法: 复制文件( 适配大文件, 避免OOM)
* 【关键优化】兼容源文件为空的场景( 适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用)
* 工具方法: 复制文件( 适配大文件, 避免OOM,兼容源文件为空场景 )
* 【关键优化】适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir)调用
* @param source 源文件(可为空,为空时仅创建目标目录)
* @param target 目标文件/目录( 若源文件为空, target视为目录并创建)
* @return true=复制/创建成功, false=失败
*/
public boolean copyFile ( File source , File target ) {
// 场景1: 源文件为空 → 仅创建目标目录( 适配Activity的目录创建 调用)
// 场景1: 源文件为空 → 仅创建目标目录( 适配Activity中mBgSourceUtils.copyFile(new File(""), parentDir) 调用)
if ( source = = null | | ( source . exists ( ) & & source . length ( ) < = 0 ) ) {
if ( target = = null ) {
LogUtils . e ( TAG , " 【文件管理】目录创建失败: 目标目录对象为null " ) ;
return false ;
}
// 若target是文件, 取其父目录; 若本身是目录, 直接创建
// 若target是文件, 取其父目录; 若本身是目录, 直接创建(适配/Pictures/PowerBell目录)
File targetDir = target . isFile ( ) ? target . getParentFile ( ) : target ;
createDirWithPermission ( targetDir , " 空源文件场景-目录创建 " ) ;
createDirWithPermission ( targetDir , " 空源文件场景-目录创建( /Pictures/PowerBell下) " ) ;
LogUtils . d ( TAG , " 【文件管理】空源文件场景:目录创建完成,路径= " + targetDir . getAbsolutePath ( ) ) ;
return true ;
}
// 场景2: 正常文件复制( 源文件非空且存在)
// 场景2: 正常文件复制( 源文件非空且存在,适配系统公共目录/Pictures/PowerBell )
if ( ! source . exists ( ) | | target = = null ) {
LogUtils . e ( TAG , " 【文件管理】文件复制失败:源文件无效或目标文件为空 " ) ;
return false ;
}
// 确保目标目录存在
// 确保目标目录存在(系统公共目录需强制创建,避免权限问题)
File targetDir = target . getParentFile ( ) ;
if ( ! targetDir . exists ( ) ) {
createDirWithPermission ( targetDir , " 文件复制目标目录 " ) ;
createDirWithPermission ( targetDir , " 文件复制目标目录( /Pictures/PowerBell下) " ) ;
}
// 调用FileUtils复制( 若项目中已有该方法, 可直接复用 )
return FileUtils . copyFile( source , target ) ;
// 调用流复制方法( 适配系统公共目录权限, 避免Permission denied )
return copyFileByStream ( source , target ) ;
}
/**
* 工具方法: 清理裁剪相关临时文件( 对外提供, Activity退出时调用)
* 工具方法: 清理裁剪相关临时文件( 对外提供, Activity退出时调用,适配/Pictures/PowerBell/cache目录 )
*/
public void clearCropTempFiles ( ) {
clearOldFile ( cropTempFile , " 裁剪临时文件 " ) ;
clearOldFile ( cropResultFile , " 裁剪结果文件 " ) ;
// 清理裁剪目录下的其他临时文件( Java7 普通for循环)
if ( fCropTemp Dir . exists ( ) ) {
File [ ] files = fCropTemp Dir . listFiles ( ) ;
clearOldFile ( cropTempFile , " 裁剪临时文件( /Pictures/PowerBell/cache) " ) ;
clearOldFile ( cropResultFile , " 裁剪结果文件( /Pictures/PowerBell/BackgroundSource) " ) ;
// 清理裁剪缓存 目录下的其他临时文件( Java7 普通for循环,兼容语法 )
if ( fPictureCache Dir . exists ( ) ) {
File [ ] files = fPictureCache Dir . listFiles ( ) ;
if ( files ! = null & & files . length > 0 ) {
for ( int i = 0 ; i < files . length ; i + + ) {
File file = files [ i ] ;
if ( file . isFile ( ) ) {
// 强制设置为可写后删除(避免系统公共目录文件只读)
file . setWritable ( true , false ) ;
file . delete ( ) ;
}
}
}
}
// 新增: 清理优先裁剪目录( CropInner) 下的临时文件
if ( fCropInnerDir ! = null & & fCropInnerDir . exists ( ) ) {
File [ ] files = fCropInnerDir . listFiles ( ) ;
if ( files ! = null & & files . length > 0 ) {
for ( int i = 0 ; i < files . length ; i + + ) {
File file = files [ i ] ;
if ( file . isFile ( ) ) {
file . delete ( ) ;
}
}
}
}
LogUtils . d ( TAG , " 【文件管理】裁剪相关临时文件清理完成 " ) ;
LogUtils . d ( TAG , " 【文件管理】裁剪相关临时文件清理完成(/Pictures/PowerBell下) " ) ;
}
/**
* 对外接口: 清理指定旧文件( 适配BackgroundSettingsActivity调用)
* 对外接口: 清理指定旧文件( 适配BackgroundSettingsActivity调用,支持/Pictures/PowerBell目录 )
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
@@ -685,7 +669,7 @@ public class BackgroundSourceUtils {
}
/**
* 工具方法:获取目录类型描述(用于日志调试,明确目录类型)
* 工具方法:获取目录类型描述(用于日志调试,明确目录类型,适配新目录结构 )
* @param dir 目标目录
* @return 目录类型描述
*/
@@ -694,11 +678,14 @@ public class BackgroundSourceUtils {
return " 未知目录( null) " ;
}
String dirPath = dir . getAbsolutePath ( ) ;
String publicPicturePath = Environment . getExternalStoragePublicDirectory ( Environment . DIRECTORY_PICTURES ) . getAbsolutePath ( ) ;
String externalFilesPath = mContext . getExternalFilesDir ( null ) ! = null ? mContext . getExternalFilesDir ( null ) . getAbsolutePath ( ) : " " ;
String cachePath = mContext . getCacheDir ( ) . getAbsolutePath ( ) ;
if ( ! TextUtils . isEmpty ( externalFiles Path) & & dirPath . contains ( externalFilesPath ) ) {
return " 应用私有外部目录( getExternalFilesDir(),系统裁剪可读写 ) " ;
if ( ! TextUtils . isEmpty ( publicPicture Path) & & dirPath . contains ( publicPicturePath + File . separator + " PowerBell " ) ) {
return " 系统公共图片目录(/Pictures/PowerBell, 图片存储/裁剪目录 ) " ;
} else if ( ! TextUtils . isEmpty ( externalFilesPath ) & & dirPath . contains ( externalFilesPath ) ) {
return " 应用私有外部目录( getExternalFilesDir(), JSON配置目录) " ;
} else if ( dirPath . contains ( cachePath ) ) {
return " 应用内部缓存目录( getCacheDir(),兜底目录) " ;
} else {