Compare commits

..

18 Commits

Author SHA1 Message Date
a44f7fe6d4 <powerbell>APK 15.12.15 release Publish. 2025-12-11 20:54:28 +08:00
35a34b5b53 <powerbell>Start New Stage Version. 2025-12-11 20:51:08 +08:00
d35d0d0291 图片选择到剪裁选定流程调试完成。 2025-12-11 20:49:58 +08:00
03212c0554 一次不必要的数据清空操作 2025-12-11 19:58:39 +08:00
0c963213df 调试信息准备 2025-12-11 19:40:30 +08:00
10ddca4f73 调试信息准备 2025-12-11 19:34:47 +08:00
f240d9c057 视图控件与全局位图缓存类优先调整。 2025-12-11 19:18:02 +08:00
2c77bf775b 背景图片控件测试通过,现在的问题就剩下,应用全局位图缓存问题与图片路径传递问题。 2025-12-11 18:48:42 +08:00
1db7c9bf80 20251211_130730_716 2025-12-11 13:07:35 +08:00
fd556fd06f 20251211_130438_907 2025-12-11 13:04:44 +08:00
220aa9dbfb 应用权限框架更新,重新调试剪裁文件流程。 2025-12-11 09:02:41 +08:00
ecafd2026f 相册权限申请模块改进中。。。 2025-12-11 06:53:22 +08:00
6ed9bc0d8e <powerbell>APK 15.12.14 release Publish. 2025-12-11 03:22:10 +08:00
bcb5db0a17 移除启动页cache缓存,以及移除savedInstanceState 窗体缓存。 2025-12-11 03:18:03 +08:00
6b69e04706 <powerbell>APK 15.12.13 release Publish. 2025-12-11 03:04:35 +08:00
a2a61bbf0b 添加BitmapCacheUtils应用全局位图缓存工具类。加速位图加载。 2025-12-11 03:02:46 +08:00
9f4211c83e <powerbell>APK 15.12.12 release Publish. 2025-12-10 20:37:42 +08:00
447a786632 图片尺寸调整函数优化 2025-12-10 20:31:10 +08:00
14 changed files with 1766 additions and 1601 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Dec 10 20:11:13 HKT 2025
stageCount=12
#Thu Dec 11 20:54:28 HKT 2025
stageCount=16
libraryProject=
baseVersion=15.12
publishVersion=15.12.11
publishVersion=15.12.15
buildCount=0
baseBetaVersion=15.12.12
baseBetaVersion=15.12.16

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

@@ -2,16 +2,22 @@ package cc.winboll.studio.powerbell;
import android.content.Context;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
import java.io.File;
public class App extends GlobalApplication {
public static final String TAG = "App";
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
@@ -24,6 +30,9 @@ public class App extends GlobalApplication {
// 数据配置存储工具
static AppConfigUtils _mAppConfigUtils;
static AppCacheUtils _mAppCacheUtils;
// 新增:全局 Bitmap 缓存工具(常驻内存)
public static BitmapCacheUtils _mBitmapCacheUtils;
GlobalApplicationReceiver mReceiver;
static String szTempDir = "";
@@ -35,24 +44,15 @@ public class App extends GlobalApplication {
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
//setIsDebugging(false);
// 初始化活动窗口管理
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
// 临时文件夹方案1
// 获取Pictures文件夹路径Android 10及以上推荐使用MediaStore此处为传统方式
// 临时文件夹初始化(保持原有逻辑)
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
// 定义目标文件路径在Pictures目录下创建"PowerBell"子文件夹及文件)
File powerBellDir = new File(picturesDir, "PowerBell");
// 临时文件夹方案2 <图片保存失败>
// 获取Pictures文件夹路径Android 10及以上推荐使用MediaStore此处为传统方式
//File powerBellDir = getExternalFilesDir("TempDir");
// 先创建文件夹(如果不存在)
if (!powerBellDir.exists()) {
powerBellDir.mkdirs();
}
@@ -61,11 +61,54 @@ public class App extends GlobalApplication {
// 设置数据配置存储工具
_mAppConfigUtils = getAppConfigUtils(this);
_mAppCacheUtils = getAppCacheUtils(this);
// 初始化全局 Bitmap 缓存工具关键App 启动时初始化,常驻内存)
_mBitmapCacheUtils = BitmapCacheUtils.getInstance();
mReceiver = new GlobalApplicationReceiver(this);
mReceiver.registerAction();
// ======================== 新增:异步预加载背景图 ========================
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 1. 获取背景源工具类实例
BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(App.this);
if (bgSourceUtils == null) {
LogUtils.e(TAG, "preloadBitmap: BackgroundSourceUtils 实例为空");
return;
}
// 2. 获取当前背景Bean
BackgroundBean bgBean = bgSourceUtils.getCurrentBackgroundBean();
if (bgBean == null || !bgBean.isUseBackgroundFile()) {
LogUtils.d(TAG, "preloadBitmap: 无有效背景文件,跳过预加载");
return;
}
// 3. 获取背景图路径(优先取压缩图路径)
String bgPath = bgBean.isUseBackgroundScaledCompressFile()
? bgBean.getBackgroundScaledCompressFilePath()
: bgBean.getBackgroundFilePath();
// 4. 预加载到全局缓存
if (_mBitmapCacheUtils != null) {
_mBitmapCacheUtils.cacheBitmap(bgPath);
LogUtils.d(TAG, "preloadBitmap: 应用启动时预加载成功 - " + bgPath);
} else {
LogUtils.e(TAG, "preloadBitmap: 全局 BitmapCacheUtils 未初始化");
}
} catch (Exception e) {
LogUtils.e(TAG, "preloadBitmap: 预加载失败 - " + e.getMessage());
}
}
}).start();
}
}, 1000); // 延迟1秒执行避免阻塞应用初始化
// ======================== 预加载逻辑结束 ========================
}
// 保持原有方法不变
public static AppConfigUtils getAppConfigUtils(Context context) {
if (_mAppConfigUtils == null) {
_mAppConfigUtils = AppConfigUtils.getInstance(context);
@@ -88,8 +131,10 @@ public class App extends GlobalApplication {
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
// 可选App 终止时清空 Bitmap 缓存,释放内存
if (_mBitmapCacheUtils != null) {
_mBitmapCacheUtils.clearAllCache();
}
}
}

View File

@@ -2,7 +2,6 @@ package cc.winboll.studio.powerbell;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
@@ -25,7 +24,6 @@ import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.activitys.AboutActivity;
import cc.winboll.studio.libaes.models.APPInfo;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
@@ -51,33 +49,22 @@ import cc.winboll.studio.powerbell.views.BatteryDrawable;
import cc.winboll.studio.powerbell.views.VerticalSeekBar;
/**
* 主活动类Java 7 兼容 | 首屏加速+窗体缓存)
* 主活动类Java 7 兼容 | 移除窗体缓存)
* 核心优化点:
* 1. ViewHolder 缓存控件 + ViewStub 延迟加载(原有)
* 2. savedInstanceState 窗体缓存(新增):缓存视图状态/数据,快速重建窗体
* 3. 资源异步加载 + 非关键任务异步化(原有)
* 1. ViewHolder 缓存控件 + ViewStub 延迟加载
* 2. 资源异步加载 + 非关键任务异步化
*/
public class MainActivity extends WinBoLLActivity {
// ======================== 静态常量(新增savedInstanceState 缓存键)========================
// ======================== 静态常量(移除缓存相关键)========================
public static final String TAG = "MainActivity";
private static final int REQUEST_WRITE_STORAGE_PERMISSION = 1001;
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
public static final int MSG_RELOAD_APPCONFIG = 0;
public static final int MSG_CURRENTVALUEBATTERY = 1;
public static final int MSG_LOAD_BACKGROUND = 2;
private static final int DELAY_LOAD_NON_CRITICAL = 500;
// 缓存键(统一管理,避免键名重复)
private static final String KEY_VIEW_STATE = "key_view_state"; // 视图状态缓存键
private static final String KEY_CHARGE_REMINDER = "key_charge_reminder"; // 充电提醒值
private static final String KEY_USEGE_REMINDER = "key_usege_reminder"; // 耗电提醒值
private static final String KEY_CURRENT_VALUE = "key_current_value"; // 当前电量值
private static final String KEY_IS_SERVICE_ENABLE = "key_is_service_enable"; // 服务开关状态
private static final String KEY_IS_CHARGE_ENABLE = "key_is_charge_enable"; // 充电提醒开关
private static final String KEY_IS_USEGE_ENABLE = "key_is_usege_enable"; // 耗电提醒开关
private static final String KEY_BACKGROUND_BEAN = "key_background_bean"; // 背景配置Bean
// ======================== 成员属性(保持原有,新增缓存标识)========================
// ======================== 成员属性(移除缓存标识)========================
public static MainActivity _mMainActivity;
static MainViewFragment _mMainViewFragment;
static Handler _mHandler;
@@ -99,10 +86,6 @@ public class MainActivity extends WinBoLLActivity {
private BatteryDrawable mChargeReminderValueBatteryDrawable;
private BatteryDrawable mUsegeReminderValueBatteryDrawable;
// 新增缓存标识判断是否从savedInstanceState恢复避免重复初始化
private boolean isRestoredFromCache = false;
// ======================== 视图缓存容器ViewHolder 不变)========================
private static class ViewHolder {
BackgroundView backgroundView;
@@ -115,8 +98,7 @@ public class MainActivity extends WinBoLLActivity {
ImageView ivCurrentBattery, ivChargeReminderBattery, ivUsegeReminderBattery;
}
// ======================== 重写生命周期核心savedInstanceState 缓存/恢复)========================
// ======================== 重写生命周期(移除缓存恢复/保存逻辑)========================
@Override
public Activity getActivity() {
return this;
@@ -129,80 +111,27 @@ public class MainActivity extends WinBoLLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
LogUtils.d(TAG, "onCreate(...) - 窗体缓存优化savedInstanceState = " + (savedInstanceState != null));
LogUtils.d(TAG, "onCreate(...) - 移除窗体缓存");
super.onCreate(savedInstanceState);
initGlobalHandler();
// 第一步优先从savedInstanceState恢复窗体核心优化
if (savedInstanceState != null) {
restoreFromCache(savedInstanceState); // 从缓存恢复视图/数据
isRestoredFromCache = true; // 标记为缓存恢复,跳过重复初始化
// 恢复原始初始化流程:直接加载布局+初始化
setContentView(R.layout.activity_main);
initCoreUtilsAsync();
initViewHolder();
initCriticalView();
loadNonCriticalViewDelayed();
// 权限申请
PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES);
}
// 第二步:无缓存时,正常初始化(保持原有逻辑,新增判断)
if (!isRestoredFromCache) {
setContentView(R.layout.activity_main); // 加载布局(仅首次/无缓存时执行)
initCoreUtilsAsync(); // 异步初始化工具类
initViewHolder(); // 绑定控件(仅首次/无缓存时执行)
initCriticalView(); // 初始化核心视图
loadNonCriticalViewDelayed(); // 延迟加载非核心视图
}
// 第三步:权限申请(无论是否缓存,都需确保权限有效)
PermissionUtils.getInstance().checkAndRequestStoragePermission(this);
}
/**
* 重写 onSaveInstanceState保存视图状态/数据到 savedInstanceState关键
* 当Activity销毁前如旋转屏幕、后台回收自动调用此方法缓存数据
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
LogUtils.d(TAG, "onSaveInstanceState - 缓存窗体状态/数据");
// 1. 缓存控件数据(避免重建后重复读取配置)
if (mViewHolder != null && mAppConfigUtils != null) {
outState.putInt(KEY_CHARGE_REMINDER, mAppConfigUtils.getChargeReminderValue());
outState.putInt(KEY_USEGE_REMINDER, mAppConfigUtils.getUsegeReminderValue());
outState.putInt(KEY_CURRENT_VALUE, mAppConfigUtils.getCurrentValue());
outState.putBoolean(KEY_IS_SERVICE_ENABLE, mAppConfigUtils.getIsEnableService());
outState.putBoolean(KEY_IS_CHARGE_ENABLE, mViewHolder.cbIsEnableChargeReminder.isChecked());
outState.putBoolean(KEY_IS_USEGE_ENABLE, mViewHolder.cbIsEnableUsegeReminder.isChecked());
}
// 2. 缓存背景配置(避免重建后重复加载背景图)
if (mBgSourceUtils != null) {
BackgroundBean backgroundBean = mBgSourceUtils.getCurrentBackgroundBean();
if (backgroundBean != null) {
outState.putSerializable(KEY_BACKGROUND_BEAN, backgroundBean); // 需BackgroundBean实现Serializable
}
}
// 3. 缓存视图状态可选如SeekBar进度、控件可见性等
if (mViewHolder != null) {
outState.putInt(KEY_VIEW_STATE + "_charge_seek", mViewHolder.chargeReminderSeekBar.getProgress());
outState.putInt(KEY_VIEW_STATE + "_usege_seek", mViewHolder.usegeReminderSeekBar.getProgress());
}
}
/**
* 重写 onRestoreInstanceState从 savedInstanceState 恢复数据可选onCreate中已处理
* 确保在onStart后、onResume前恢复避免视图状态覆盖
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null && !isRestoredFromCache) {
restoreFromCache(savedInstanceState);
isRestoredFromCache = true;
}
}
// 移除 onSaveInstanceState 方法
// 移除 onRestoreInstanceState 方法
@Override
protected void onDestroy() {
super.onDestroy();
// 释放资源(保持原有)
if (mADsBannerView != null) {
mADsBannerView.releaseAdResources();
}
@@ -211,23 +140,15 @@ public class MainActivity extends WinBoLLActivity {
if (_mHandler != null) {
_mHandler.removeCallbacksAndMessages(null);
}
// 重置缓存标识
isRestoredFromCache = false;
}
@Override
protected void onResume() {
super.onResume();
// 缓存恢复后,无需重复发送加载消息(仅首次/无缓存时执行)
if (!isRestoredFromCache) {
// 移除缓存恢复后的刷新逻辑,直接发送加载背景消息
if (_mHandler != null) {
_mHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
}
} else {
// 缓存恢复后,快速刷新视图(避免状态不一致)
refreshViewFromCache();
}
// 恢复广告(保持原有)
if (mADsBannerView != null) {
mADsBannerView.resumeADs(MainActivity.this);
}
@@ -240,6 +161,7 @@ public class MainActivity extends WinBoLLActivity {
AESThemeUtil.inflateMenu(this, menu);
if (App.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu);
}
getMenuInflater().inflate(R.menu.toolbar_main, mMenu);
return true;
@@ -294,6 +216,14 @@ public class MainActivity extends WinBoLLActivity {
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
PermissionUtils.getInstance().handleStoragePermissionResult(this, requestCode, permissions, grantResults);
}
}
@Override
public void onBackPressed() {
moveTaskToBack(true);
@@ -312,94 +242,10 @@ public class MainActivity extends WinBoLLActivity {
}
}
// ======================== 新增:窗体缓存核心方法(恢复+刷新)========================
/**
* 从 savedInstanceState 恢复视图和数据(核心)
* 避免重复加载布局、绑定控件、读取配置,加快窗体重建速度
*/
private void restoreFromCache(Bundle savedInstanceState) {
LogUtils.d(TAG, "restoreFromCache - 从缓存恢复窗体");
// 1. 恢复工具类(无需重新初始化,直接获取实例)
mApplication = (App) getApplication();
mAppConfigUtils = App.getAppConfigUtils(getActivity());
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
// 2. 恢复ViewHolder仅绑定控件不重新inflate布局
if (mViewHolder == null) {
mViewHolder = new ViewHolder();
initViewHolder(); // 复用原有绑定逻辑,避免重复代码
}
// 3. 恢复背景配置直接设置缓存的BackgroundBean避免重复读取
BackgroundBean cachedBean = (BackgroundBean) savedInstanceState.getSerializable(KEY_BACKGROUND_BEAN);
if (cachedBean != null && mViewHolder.backgroundView != null) {
mViewHolder.backgroundView.loadBackgroundBean(cachedBean);
//mBgSourceUtils.setCurrentBackgroundBean(cachedBean); // 同步更新工具类缓存
}
// 4. 恢复控件数据从缓存读取避免重复调用AppConfigUtils
int chargeReminder = savedInstanceState.getInt(KEY_CHARGE_REMINDER, 80); // 默认值80
int usegeReminder = savedInstanceState.getInt(KEY_USEGE_REMINDER, 20); // 默认值20
int currentValue = savedInstanceState.getInt(KEY_CURRENT_VALUE, 50); // 默认值50
boolean isServiceEnable = savedInstanceState.getBoolean(KEY_IS_SERVICE_ENABLE, false);
boolean isChargeEnable = savedInstanceState.getBoolean(KEY_IS_CHARGE_ENABLE, false);
boolean isUsegeEnable = savedInstanceState.getBoolean(KEY_IS_USEGE_ENABLE, false);
// 5. 恢复电量图标(复用缓存实例,避免重新创建)
initBatteryDrawables();
mCurrentValueBatteryDrawable.setValue(currentValue);
mChargeReminderValueBatteryDrawable.setValue(chargeReminder);
mUsegeReminderValueBatteryDrawable.setValue(usegeReminder);
// 6. 恢复控件状态(直接设置,避免重复绑定数据)
if (mViewHolder != null) {
mViewHolder.cbIsEnableChargeReminder.setChecked(isChargeEnable);
mViewHolder.cbIsEnableUsegeReminder.setChecked(isUsegeEnable);
mViewHolder.swIsEnableService.setChecked(isServiceEnable);
mViewHolder.chargeReminderSeekBar.setProgress(savedInstanceState.getInt(KEY_VIEW_STATE + "_charge_seek", chargeReminder));
mViewHolder.usegeReminderSeekBar.setProgress(savedInstanceState.getInt(KEY_VIEW_STATE + "_usege_seek", usegeReminder));
mViewHolder.tvChargeReminderValue.setText(String.valueOf(chargeReminder) + "%");
mViewHolder.tvUsegeReminderValue.setText(String.valueOf(usegeReminder) + "%");
mViewHolder.tvCurrentValue.setText(String.valueOf(currentValue) + "%");
}
// 7. 恢复非核心视图(广告)
loadAdsView();
}
// ======================== 移除缓存相关核心方法restoreFromCache/refreshViewFromCache========================
/**
* 缓存恢复后,快速刷新视图(确保状态一致
*/
private void refreshViewFromCache() {
if (mViewHolder == null) return;
// 快速设置背景色从缓存Bean获取
if (mBgSourceUtils != null && mViewHolder.mainLayout != null) {
BackgroundBean cachedBean = mBgSourceUtils.getCurrentBackgroundBean();
if (cachedBean != null) {
mViewHolder.mainLayout.setBackgroundColor(cachedBean.getPixelColor());
}
}
// 快速设置电量图标(避免图标不显示)
if (mViewHolder.ivCurrentBattery != null) {
mViewHolder.ivCurrentBattery.setImageDrawable(mCurrentValueBatteryDrawable);
}
if (mViewHolder.ivChargeReminderBattery != null) {
mViewHolder.ivChargeReminderBattery.setImageDrawable(mChargeReminderValueBatteryDrawable);
}
if (mViewHolder.ivUsegeReminderBattery != null) {
mViewHolder.ivUsegeReminderBattery.setImageDrawable(mUsegeReminderValueBatteryDrawable);
}
// 重新绑定监听(确保交互正常,避免监听失效)
setViewListener();
}
/**
* 初始化电量图标(复用逻辑,避免重复代码)
* 初始化电量图标(复用逻辑
*/
private void initBatteryDrawables() {
if (mCurrentValueBatteryDrawable == null) {
@@ -413,13 +259,11 @@ public class MainActivity extends WinBoLLActivity {
}
}
// ======================== 原有核心方法(微调:新增缓存判断,避免重复执行)========================
// ======================== 原有核心方法(移除缓存判断逻辑)========================
/**
* 刷新背景(新增缓存判断,缓存恢复后无需重复执行
* 刷新背景(移除缓存判断)
*/
public void reloadBackground() {
if (isRestoredFromCache) return; // 缓存恢复后,跳过重复加载
if (mViewHolder == null || mBgSourceUtils == null || mViewHolder.backgroundView == null) {
LogUtils.e(TAG, "reloadBackground: 背景加载失败(控件/工具类未初始化)");
return;
@@ -434,10 +278,9 @@ public class MainActivity extends WinBoLLActivity {
}
/**
* 设置主页面背景颜色(新增缓存判断)
* 设置主页面背景颜色(移除缓存判断)
*/
void setBackgroundColor() {
if (isRestoredFromCache) return;
if (isFinishing() || isDestroyed() || mViewHolder == null || mBgSourceUtils == null || mViewHolder.mainLayout == null) {
return;
}
@@ -449,10 +292,9 @@ public class MainActivity extends WinBoLLActivity {
}
/**
* 设置视图数据(新增缓存判断,缓存恢复后无需重复执行
* 设置视图数据(移除缓存判断)
*/
void setViewData() {
if (isRestoredFromCache) return;
if (mViewHolder == null || mAppConfigUtils == null) return;
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
@@ -464,7 +306,7 @@ public class MainActivity extends WinBoLLActivity {
mViewHolder.llRightSeekBar.setBackground(mDrawableFrame);
}
initBatteryDrawables(); // 复用初始化逻辑
initBatteryDrawables();
if (mViewHolder.ivCurrentBattery != null) {
mCurrentValueBatteryDrawable.setValue(nCurrentValue);
mViewHolder.ivCurrentBattery.setImageDrawable(mCurrentValueBatteryDrawable);
@@ -478,7 +320,6 @@ public class MainActivity extends WinBoLLActivity {
mViewHolder.ivUsegeReminderBattery.setImageDrawable(mUsegeReminderValueBatteryDrawable);
}
// 控件数据绑定(保持原有)
if (mViewHolder.tvChargeReminderValue != null) {
mViewHolder.tvChargeReminderValue.setTextColor(getActivity().getColor(R.color.colorCharge));
mViewHolder.tvChargeReminderValue.setText(String.valueOf(nChargeReminderValue) + "%");
@@ -515,7 +356,6 @@ public class MainActivity extends WinBoLLActivity {
}
}
// 其他原有方法setViewListener、setCurrentValueBattery、initCoreUtilsAsync等保持不变
/**
* 设置视图监听(保持原有)
*/

View File

@@ -18,6 +18,7 @@ import cc.winboll.studio.powerbell.utils.PermissionUtils;
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "SettingsActivity";
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
private Toolbar mToolbar;
@@ -52,8 +53,15 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
public void onCheckPermission(View view) {
//ToastUtils.show("onCheckPermission");
if (PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
ToastUtils.show("【权限检查】存储权限已全部获取");
PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
PermissionUtils.getInstance().handleStoragePermissionResult(this, requestCode, permissions, grantResults);
}
}
}

View File

@@ -35,7 +35,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
BackgroundView mBackgroundView;
// 测试图片路径用Environment获取适配低版本避免硬编码
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
+ "/PowerBell/unittest/1764946782079.jpeg";
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -90,7 +90,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
}
String dstOutputPath = outputDir.getAbsolutePath()
+ "/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
+ "/1764946782079-crop.jpeg";
// 修复3自由裁剪时比例传0避免100:100过大导致机型崩溃
ImageCropUtils.startImageCrop(
@@ -161,7 +161,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
// 裁剪完成后回收权限
if (requestCode == REQUEST_CROP_IMAGE) {
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
+ "/PowerBell/unittest/1764946782079-crop.jpeg";
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
//ImageCropUtils.releaseCropPermission(this, outputUri);
mBackgroundView.loadImage(dstOutputPath);

View File

@@ -0,0 +1,122 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/11 09:14
* @Describe Assets 目录拷贝工具类
* 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径
*/
public class AssetsCopyUtils {
public static final String TAG = "AssetsCopyUtils";
private static final int BUFFER_SIZE = 1024 * 8;
/**
* 拷贝 assets/images/ 目录到指定目标目录
* @param context 上下文
* @param targetDirPath 目标目录完整路径(如 /sdcard/PowerBell/assets_images
* @return 拷贝是否成功
*/
public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) {
// 拷贝 assets/images 根目录
return copyAssetsDirToDir(context, "images", targetDirPath);
}
/**
* 递归拷贝 assets 下指定目录到目标目录
* @param context 上下文
* @param assetsDir assets 下的源目录(如 "images"、"images/subdir"
* @param targetDirPath 目标目录完整路径
* @return 拷贝是否成功
*/
public static boolean copyAssetsDirToDir(Context context, String assetsDir, String targetDirPath) {
File targetDir = new File(targetDirPath);
// 创建目标目录(含多级父目录)
if (!targetDir.exists() && !targetDir.mkdirs()) {
Log.e(TAG, "创建目标目录失败:" + targetDirPath);
return false;
}
try {
// 获取 assets 目录下的文件/子目录列表
String[] fileList = context.getAssets().list(assetsDir);
if (fileList == null || fileList.length == 0) {
Log.d(TAG, "assets 目录为空:" + assetsDir);
return true;
}
for (String fileName : fileList) {
String assetsFilePath = assetsDir + File.separator + fileName;
String targetFilePath = targetDirPath + File.separator + fileName;
// 判断当前项是文件还是子目录
String[] subFileList = context.getAssets().list(assetsFilePath);
if (subFileList != null && subFileList.length > 0) {
// 是子目录,递归拷贝
if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) {
return false;
}
} else {
// 是文件,直接拷贝
if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) {
return false;
}
}
}
Log.d(TAG, "assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
return true;
} catch (IOException e) {
Log.e(TAG, "拷贝 assets 目录异常:" + e.getMessage());
return false;
}
}
/**
* 拷贝 assets 下单个文件到指定路径
* @param context 上下文
* @param assetsFilePath assets 下的文件路径(如 "images/cloud.png"
* @param targetFilePath 目标文件完整路径
* @return 拷贝是否成功
*/
public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String targetFilePath) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = context.getAssets().open(assetsFilePath);
File targetFile = new File(targetFilePath);
// 覆盖已存在的文件
if (targetFile.exists() && !targetFile.delete()) {
Log.w(TAG, "覆盖目标文件失败,跳过:" + targetFilePath);
return true;
}
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
Log.d(TAG, "文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
return true;
} catch (IOException e) {
Log.e(TAG, "拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage());
return false;
} finally {
// 关闭流
try {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
} catch (IOException e) {
Log.e(TAG, "关闭流异常:" + e.getMessage());
}
}
}
}

View File

@@ -4,9 +4,10 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.BuildConfig;
@@ -19,8 +20,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import android.os.Build;
import androidx.core.content.FileProvider;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -31,54 +30,44 @@ public class BackgroundSourceUtils {
public static final String TAG = "BackgroundSourceUtils";
// 裁剪相关常量(统一定义,避免硬编码)
private static final String CROP_CACHE_DIR_NAME = "cache"; // 裁剪缓存目录(基础目录下)
private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; // 裁剪输入临时文件
private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; // 裁剪输出结果文件
public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容
// 图片操作基础目录(核心:系统公共图片目录)
private static final String CROP_CACHE_DIR_NAME = "cache";
private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg";
private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg";
public 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";
// 新增:压缩图统一存储目录(图片基础目录下/BackgroundCrops
private static final String SOURCE_DIR_NAME = "BackgroundSource";
private static final String COMPRESS_DIR_NAME = "BackgroundCompress";
// 1. 静态实例加volatile禁止指令重排保证可见性双重校验锁单例核心
// 单例相关
private static volatile BackgroundSourceUtils sInstance;
private Context mContext;
private File currentBackgroundBeanFile;
private BackgroundBean currentBackgroundBean; // 正式Bean独立实例
private BackgroundBean currentBackgroundBean;
private File previewBackgroundBeanFile;
private BackgroundBean previewBackgroundBean; // 预览Bean独立实例与正式Bean完全分离
private BackgroundBean previewBackgroundBean;
// 2. 统一文件目录分两类图片目录→系统公共目录JSON目录→应用外置存储
// 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/
private File fPictureBaseDir; // 图片基础目录
private File fCropCacheDir; // 裁剪缓存目录(基础目录下/cache
private File fBackgroundSourceDir; // 图片存储目录(基础目录下,存储正式/预览原图)
private File fBackgroundCompressDir; // 新增:压缩图统一存储目录(基础目录下/BackgroundCrops
// JSON配置目录原应用外置存储目录不改变
private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils
private File fModelDir; // 模型文件目录存储JSON配置
// 裁剪文件统一放入图片基础目录下的cache
private File mCropSourceFile; // 裁剪临时文件fCropCacheDir下
private File mCropResultFile; // 裁剪临时文件fCropCacheDir下
// 目录文件相关
private File fPictureBaseDir;
private File fCropCacheDir;
private File fBackgroundSourceDir;
private File fBackgroundCompressDir;
private File fUtilsDir;
private File fModelDir;
private File mCropSourceFile;
private File mCropResultFile;
// 3. 私有构造器(加防反射逻辑+初始化所有目录/文件)
// 双重校验锁单例
private BackgroundSourceUtils(Context context) {
// 防反射破坏:若已有实例,抛异常阻止重复创建
if (sInstance != null) {
throw new RuntimeException("BackgroundSourceUtils 是单例类,禁止重复创建!");
}
// 上下文用Application Context避免Activity内存泄漏
this.mContext = context.getApplicationContext();
// 【核心调整1】实例化初期优先初始化所有必要目录确保实例化完成时目录100%就绪)
initNecessaryDirs();
// 初始化所有文件(裁剪临时文件/结果文件等)
initAllFiles();
// 加载配置(确保正式/预览Bean是两份独立实例
loadSettings();
}
// 4. 双重校验锁单例线程安全高效支持多线程并发调用Java7语法兼容
public static BackgroundSourceUtils getInstance(Context context) {
if (sInstance == null) {
synchronized (BackgroundSourceUtils.class) {
@@ -91,172 +80,153 @@ public class BackgroundSourceUtils {
}
/**
* 【核心新增】统一初始化所有必要目录(实例化初期调用,确保目录优先创建)
* 整合图片目录+JSON目录集中管理目录创建逻辑保证实例化完成时所有目录就绪
* 统一初始化所有必要目录
*/
private void initNecessaryDirs() {
LogUtils.d(TAG, "实例化初期-目录初始化】开始创建所有必要目录...");
// 1. 初始化图片操作目录(系统公共目录 /Pictures/PowerBell/
LogUtils.d(TAG, "【目录初始化】开始创建所有必要目录");
initPictureDirs();
// 2. 初始化JSON配置目录应用外置存储
initJsonDirs();
LogUtils.d(TAG, "实例化初期-目录初始化】所有必要目录创建完成");
LogUtils.d(TAG, "【目录初始化】所有必要目录创建完成");
}
/**
* 初始化图片操作目录(核心:系统公共图片目录 /Pictures/PowerBell/,新增压缩图目录)
* 【调整强化】新增目录创建后二次校验,失败则降级到备选目录,确保目录可用
* 初始化图片操作目录
*/
private void initPictureDirs() {
// 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell
fPictureBaseDir = new File(PICTURE_BASE_DIR);
// 2. 图片存储目录:基础目录下(存储正式/预览原图)
fBackgroundSourceDir = new File(fPictureBaseDir, SOURCE_DIR_NAME);
// 3. 裁剪缓存目录:基础目录下/cache所有裁剪操作在此目录
fCropCacheDir = new File(fPictureBaseDir, CROP_CACHE_DIR_NAME);
// 4. 新增:压缩图统一存储目录(基础目录下/BackgroundCrops所有压缩图放这里
fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_DIR_NAME);
// 5. 强制创建所有图片目录(带二次校验+降级兜底)
createDirWithPermission(fPictureBaseDir, "图片基础目录(" + PICTURE_BASE_DIR + "");
createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下/" + SOURCE_DIR_NAME + "");
createDirWithPermission(fCropCacheDir, "裁剪缓存目录(基础目录/" + CROP_CACHE_DIR_NAME + "");
createDirWithPermission(fBackgroundCompressDir, "裁剪压缩图存储目录(基础目录/" + COMPRESS_DIR_NAME + "");
createDirWithPermission(fPictureBaseDir, "图片基础目录");
createDirWithPermission(fBackgroundSourceDir, "图片存储目录");
createDirWithPermission(fCropCacheDir, "裁剪缓存目录");
createDirWithPermission(fBackgroundCompressDir, "压缩图存储目录");
// 6. 目录创建后最终校验(确保所有目录已就绪)
validatePictureDirs();
LogUtils.d(TAG, "【图片目录初始化】完成:" +
"基础目录=" + fPictureBaseDir.getAbsolutePath() +
"图片存储目录=" + fBackgroundSourceDir.getAbsolutePath() +
",裁剪缓存目录=" + fCropCacheDir.getAbsolutePath() +
",裁剪压缩图存储目录=" + fBackgroundCompressDir.getAbsolutePath());
}
/**
* 初始化JSON配置目录(保留原逻辑:应用外置存储)
* 【调整强化】新增目录创建后二次校验,失败则降级到应用内部缓存目录
* 初始化JSON配置目录
*/
private void initJsonDirs() {
// 1. 工具类根目录(应用外置存储)
fUtilsDir = mContext.getExternalFilesDir(TAG);
if (fUtilsDir == null) {
LogUtils.e(TAG, "【JSON目录】应用外置存储不可用,切换到应用内部缓存目录");
LogUtils.e(TAG, "应用外置存储不可用,切换到应用内部缓存目录");
fUtilsDir = mContext.getDataDir();
}
// 2. 模型文件目录存储JSON配置
fModelDir = new File(fUtilsDir, "ModelDir");
// 强制创建JSON目录带二次校验+降级兜底)
createDirWithPermission(fModelDir, "JSON配置目录应用外置存储");
createDirWithPermission(fModelDir, "JSON配置目录");
// 3. 初始化JSON文件对象两份独立文件对应两份Bean实例
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
LogUtils.d(TAG, "【JSON目录初始化】完成目录=" + fModelDir.getAbsolutePath() + "正式JSON=" + currentBackgroundBeanFile.getName() + "预览JSON=" + previewBackgroundBeanFile.getName());
}
/**
* 【核心强化】创建目录并设置权限(适配系统公共目录/Pictures/PowerBell确保实例化时目录就绪
* @param dir 要创建的目录
* @param dirDesc 目录描述(用于日志打印)
* 创建目录并校验
*/
private void createDirWithPermission(File dir, String dirDesc) {
if (dir == null) {
LogUtils.e(TAG, "【文件管理】创建目录失败目录对象为null描述" + dirDesc + "");
LogUtils.e(TAG, dirDesc + "创建失败目录对象为null");
return;
}
// 第一步:主动检测并创建目录(递归创建所有父目录)
if (!dir.exists()) {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "不存在,开始创建:" + dir.getAbsolutePath());
dir.mkdirs(); // 递归创建所有父目录
} else {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "已存在:" + dir.getAbsolutePath());
LogUtils.d(TAG, dirDesc + "不存在,开始创建:" + dir.getAbsolutePath());
dir.mkdirs();
}
}
/**
* 【新增】图片目录创建后最终校验(确保实例化时所有图片目录就绪
* 校验图片目录是否就绪
*/
private void validatePictureDirs() {
LogUtils.d(TAG, "【图片目录校验】开始校验所有图片目录...");
boolean allReady = true;
if (!fPictureBaseDir.exists() || !fPictureBaseDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】图片基础目录未就绪:" + fPictureBaseDir.getAbsolutePath());
allReady = false;
}
if (!fBackgroundSourceDir.exists() || !fBackgroundSourceDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】图片存储目录未就绪:" + fBackgroundSourceDir.getAbsolutePath());
allReady = false;
}
if (!fCropCacheDir.exists() || !fCropCacheDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】裁剪缓存目录未就绪:" + fCropCacheDir.getAbsolutePath());
allReady = false;
}
if (!fBackgroundCompressDir.exists() || !fBackgroundCompressDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】压缩图目录未就绪:" + fBackgroundCompressDir.getAbsolutePath());
allReady = false;
}
boolean allReady = fPictureBaseDir.exists() && fBackgroundSourceDir.exists()
&& fCropCacheDir.exists() && fBackgroundCompressDir.exists();
if (allReady) {
LogUtils.d(TAG, "【图片目录校验】所有图片目录均已就绪");
LogUtils.d(TAG, "所有图片目录均已就绪");
} else {
LogUtils.e(TAG, "【图片目录校验】部分目录未就绪,可能影响后续功能");
LogUtils.e(TAG, "部分图片目录未就绪,可能影响后续功能");
}
}
/**
* 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录)
* 初始化所有文件
*/
private void initAllFiles() {
// 1. 裁剪临时文件
//mCropSourceFile = new File(fCropCacheDir, CROP_TEMP_FILE_NAME);
// 2. 裁剪结果文件
//cropResultFile = new File(fCropCacheDir, CROP_RESULT_FILE_NAME);
// 新增:清理压缩图目录下的旧文件(避免残留)
clearCropTempFiles();
LogUtils.d(TAG, "文件初始化完成");
LogUtils.d(TAG, "文件初始化完成");
}
// 【核心实现】定义 getFileProviderUri 方法:将 File 转为 ContentUri适配 FileProvider
/**
* 将File转为ContentUri
*/
public Uri getFileProviderUri(File file) {
Log.d("BackgroundSourceUtils", "getFileProviderUri: 生成FileProvider Uri文件路径:" + file.getAbsolutePath());
LogUtils.d(TAG, "getFileProviderUri调用】文件路径:" + file.getAbsolutePath());
Uri contentUri = null;
try {
// 适配 Android 7.0+:使用 FileProvider 生成 ContentUri避免 FileUriExposedException
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
contentUri = FileProvider.getUriForFile(
mContext,
FILE_PROVIDER_AUTHORITY, // 与清单文件中一致
file
);
Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0+ 生成ContentUri" + contentUri.toString());
contentUri = FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITY, file);
LogUtils.d(TAG, "7.0+ 生成ContentUri" + contentUri.toString());
} else {
// 适配 Android 7.0 以下:直接使用 File.toURI()(兼容旧版本)
contentUri = Uri.fromFile(file);
Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0以下 生成FileUri" + contentUri.toString());
LogUtils.d(TAG, "7.0以下 生成FileUri" + contentUri.toString());
}
} catch (IllegalArgumentException e) {
// 捕获异常(如文件路径无效、授权不匹配等)
Log.e("BackgroundSourceUtils", "getFileProviderUri: 生成Uri失败异常" + e.getMessage(), e);
LogUtils.e(TAG, "生成Uri失败" + e.getMessage(), e);
contentUri = null;
}
return contentUri;
}
/*
* 创建预览数据剪裁环境
/**
* 检查背景是否为空并创建空白背景Bean
*/
boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) {
LogUtils.d(TAG, "【checkEmptyBackgroundAndCreateBlankBackgroundBean调用】开始检查背景Bean");
File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath());
if (!fCheckBackgroundFile.exists()) {
String newCropFileName = "blank10x10";
String fileSuffix = "png";
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
AssetsCopyUtils.copyAssetsFileToDir(mContext, "images/blank10x10.png", mCropSourceFile.getAbsolutePath());
try {
mCropResultFile.createNewFile();
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
loadSettings();
previewBackgroundBean.setIsUseBackgroundFile(true);
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(false);
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
saveSettings();
LogUtils.d(TAG, "背景Bean为空已创建空白背景并更新配置");
return true;
}
LogUtils.d(TAG, "背景Bean文件存在无需创建空白背景");
return false;
}
/**
* 创建并更新预览剪裁环境
*/
public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) {
LogUtils.d(TAG, "【createAndUpdatePreviewEnvironmentForCropping调用】开始初始化预览剪裁环境");
InputStream is = null;
FileOutputStream fos = null;
try {
clearCropTempFiles();
if (checkEmptyBackgroundAndCreateBlankBackgroundBean(oldPreviewBackgroundBean)) {
LogUtils.d(TAG, "空白背景创建成功,直接返回");
return true;
}
Uri uri = UriUtil.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
//String szType = mContext.getContentResolver().getType(uri);
// 2. 截取MIME类型后缀如从image/jpeg中提取jpeg【核心新增逻辑】
String fileSuffix = FileUtils.getFileSuffix(mContext, uri);
String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis();
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
@@ -272,103 +242,80 @@ public class BackgroundSourceUtils {
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile);
} else {
mCropSourceFile.createNewFile();
// 1. 打开Uri输入流兼容content:///file:// 等多种Uri格式
is = mContext.getContentResolver().openInputStream(uri);
if (is == null) {
LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败Uri" + uri.toString());
LogUtils.e(TAG, "ContentResolver打开Uri失败" + uri.toString());
return false;
}
// 2. 初始化选图临时文件输出流Java7 手动创建流不依赖try-with-resources
fos = new FileOutputStream(mCropSourceFile);
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区平衡读写性能与内存占用
int readLen; // 每次读取的字节长度
// 3. 流复制Java7 标准while循环避免Java8+语法)
byte[] buffer = new byte[1024 * 8];
int readLen;
while ((readLen = is.read(buffer)) != -1) {
fos.write(buffer, 0, readLen); // 精准写入读取到的字节,避免空字节填充
fos.write(buffer, 0, readLen);
}
// 4. 强制同步写入磁盘解决Android 10+ 异步写入导致的文件无效问题)
fos.flush();
if (fos != null) {
try {
fos.getFD().sync(); // 确保数据写入物理磁盘,而非缓存
fos.getFD().sync();
} catch (IOException e) {
LogUtils.w(TAG, "【选图解析】文件同步到磁盘失败,flush()兜底:" + e.getMessage());
LogUtils.w(TAG, "文件同步到磁盘失败flush兜底" + e.getMessage());
fos.flush();
}
}
}
// 加载图片数据模型数据
loadSettings();
// 修改预览数据模型
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
// 保存数据模型数据更改
saveSettings();
// 6. 解析成功日志(打印文件信息,便于问题排查)
LogUtils.d(TAG, "【选图解析】Uri解析成功");
LogUtils.d(TAG, "预览剪裁环境初始化成功");
LogUtils.d(TAG, "→ 原Uri" + uri.toString());
LogUtils.d(TAG, "图片剪裁数据源:" + mCropSourceFile.getAbsolutePath());
LogUtils.d(TAG, "图片剪裁数据源文件大小" + mCropSourceFile.length() + " bytes");
LogUtils.d(TAG, "→ 剪裁结果数据文件:" + mCropResultFile.getAbsolutePath());
LogUtils.d(TAG, "→ 剪裁结果数据文件大小:" + mCropResultFile.length() + " bytes");
LogUtils.d(TAG, "→ 剪裁数据源:" + mCropSourceFile.getAbsolutePath());
LogUtils.d(TAG, "剪裁结果文件" + mCropResultFile.getAbsolutePath());
return true;
} catch (Exception e) {
// 捕获所有异常IO异常/空指针等),避免崩溃
LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e);
// 异常时清理无效文件,防止残留
LogUtils.e(TAG, "预览剪裁环境初始化异常:" + e.getMessage(), e);
clearCropTempFiles();
return false;
} finally {
// 7. 手动关闭流资源Java7 标准写法,避免内存泄漏)
if (is != null) {
try {
is.close();
} catch (IOException e) {
LogUtils.e(TAG, "【选图解析】输入流关闭失败:" + e.getMessage());
LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "【选图解析】输出流关闭失败:" + e.getMessage());
LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage());
}
}
}
}
/**
* 加载背景图片配置数据核心确保current/preview是两份独立的BackgroundBean实例
* 加载背景配置
*/
public void loadSettings() {
// 1. 加载正式Bean独立实例从currentBackgroundBean.json加载不存在则新建
currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
if (currentBackgroundBean == null) {
currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化
currentBackgroundBean = new BackgroundBean();
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
LogUtils.d(TAG, "【配置管理】正式背景Bean不存在创建独立实例并保存到JSON");
LogUtils.d(TAG, "正式背景Bean不存在创建新实例");
}
// 2. 加载预览Bean独立实例从previewBackgroundBean.json加载不存在则新建与正式Bean完全分离
previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
if (previewBackgroundBean == null) {
previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化
previewBackgroundBean = new BackgroundBean();
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "【配置管理】预览背景Bean不存在创建独立实例并保存到JSON");
LogUtils.d(TAG, "预览背景Bean不存在创建新实例");
}
}
// ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------
// ------------------------------ 对外提供的核心方法 ------------------------------
public BackgroundBean getCurrentBackgroundBean() {
return currentBackgroundBean;
}
@@ -377,164 +324,109 @@ public class BackgroundSourceUtils {
return previewBackgroundBean;
}
/**
* 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验)
*/
public String getCurrentBackgroundFilePath() {
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();
}
/**
* 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验)
*/
public String getPreviewBackgroundFilePath() {
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();
}
/**
* 获取预览背景压缩图片路径(同步修复:移除 loadSettings()强化非空校验统一指向BackgroundCrops目录
*/
public String getPreviewBackgroundScaledCompressFilePath() {
String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径");
LogUtils.e(TAG, "预览压缩背景文件名为空");
return "";
}
// 关键压缩图路径统一指向BackgroundCrops目录不再用BackgroundSource
File file = new File(fBackgroundCompressDir, compressFileName);
LogUtils.d(TAG, "【路径管理】预览压缩背景路径BackgroundCrops目录" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 新增获取正式背景压缩图片路径统一指向BackgroundCrops目录对外提供调用
*/
public String getCurrentBackgroundScaledCompressFilePath() {
String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
LogUtils.e(TAG, "【路径管理】正式压缩背景文件名为空,返回空路径");
LogUtils.e(TAG, "正式压缩背景文件名为空");
return "";
}
// 关键压缩图路径统一指向BackgroundCrops目录
File file = new File(fBackgroundCompressDir, compressFileName);
LogUtils.d(TAG, "【路径管理】正式压缩背景路径BackgroundCrops目录" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 保存配置核心将两份独立Bean实例分别写入各自的JSON文件
* 保存配置
*/
public void saveSettings() {
if (currentBackgroundBean != null && previewBackgroundBean != null) {
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); // 正式Bean→正式JSON
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); // 预览Bean→预览JSON
LogUtils.d(TAG, "【配置管理】两份配置保存成功正式JSON=" + currentBackgroundBeanFile.getAbsolutePath() + "预览JSON=" + previewBackgroundBeanFile.getAbsolutePath());
return;
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "两份背景配置保存成功");
} else {
LogUtils.e(TAG, "配置保存失败current/preview Bean存在空值");
}
LogUtils.d(TAG, "【配置管理】两份配置保存失败。currentBackgroundBean 与 previewBackgroundBean 有空值。");
}
/**
* 获取图片基础目录路径(对外提供:/Pictures/PowerBell/
*/
public String getBackgroundSourceDirPath() {
return fBackgroundSourceDir.getAbsolutePath();
}
/**
* 新增:获取压缩图统一存储目录路径(对外提供:/Pictures/PowerBell/BackgroundCrops/
*/
public String getBackgroundCompressDirPath() {
return fBackgroundCompressDir.getAbsolutePath();
}
public String getCropCacheDir() {
return fCropCacheDir.getAbsolutePath();
}
public String getFileProviderAuthority() {
return FILE_PROVIDER_AUTHORITY;
}
// ------------------------------ 核心业务方法复用FileUtils简化文件操作------------------------------
// ------------------------------ 核心业务方法 ------------------------------
/**
* 优化函数仅裁剪结果图可保存到BackgroundSource避免启动裁剪时误复制原图
* 说明:启动裁剪时不调用此方法,仅在裁剪完成后保存结果图时调用
* 保存裁剪结果图到预览Bean
*/
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
final String TAG = "BackgroundSourceUtils";
// 强化校验1仅允许裁剪结果图传入通过文件路径判断避免原图误传入
LogUtils.d(TAG, "【saveFileToPreviewBean调用】源文件路径" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) {
Log.e(TAG, "【保存优化】源文件无效,拒绝保存" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
LogUtils.e(TAG, "源文件无效,拒绝保存");
return previewBackgroundBean;
}
// 强化校验2排除原图路径避免启动裁剪时传入原图复制
String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); // 原图存储目录(如相册/拍照目录)
String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath();
if (sourceFile.getAbsolutePath().contains(originalImageDir)) {
Log.w(TAG, "【保存优化】禁止复制原图到BackgroundSource,跳过保存");
LogUtils.w(TAG, "禁止复制原图,跳过保存");
return previewBackgroundBean;
}
// 确保BackgroundSource目录存在(实例化时已创建,此处二次确认)
if (!fBackgroundSourceDir.exists()) {
if (!fBackgroundSourceDir.mkdirs()) {
Log.e(TAG, "【保存优化】BackgroundSource目录创建失败");
if (!fBackgroundSourceDir.exists() && !fBackgroundSourceDir.mkdirs()) {
LogUtils.e(TAG, "BackgroundSource目录创建失败");
return previewBackgroundBean;
}
}
// 生成唯一文件名(避免覆盖)
String uniqueFileName = "bg_" + System.currentTimeMillis() + "_" + sourceFile.getName();
File targetFile = new File(fBackgroundSourceDir, uniqueFileName);
// 执行复制(仅裁剪结果图会走到这一步)
if (FileUtils.copyFile(sourceFile, targetFile)) {
Log.d(TAG, "【保存优化】裁剪结果图保存成功:" + targetFile.getAbsolutePath());
// 更新预览Bean原有逻辑保留
LogUtils.d(TAG, "裁剪结果图保存成功:" + targetFile.getAbsolutePath());
previewBackgroundBean.setBackgroundFileName(uniqueFileName);
previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath());
previewBackgroundBean.setBackgroundFileInfo(fileInfo);
previewBackgroundBean.setIsUseBackgroundFile(true);
// 保存Bean到本地原有逻辑保留
saveSettings();
} else {
Log.e(TAG, "【保存优化】裁剪结果图复制失败" + sourceFile.getAbsolutePath() + "" + targetFile.getAbsolutePath());
LogUtils.e(TAG, "裁剪结果图复制失败");
}
return previewBackgroundBean;
}
/**
* 提交预览背景到正式背景预览Bean → 正式Bean深拷贝新建正式Bean实例+逐字段拷贝)
* 核心深拷贝后修改正式Bean不会影响预览Bean两份实例完全独立压缩图路径统一指向BackgroundCrops
* 提交预览背景到正式背景
*/
public void commitPreviewSourceToCurrent() {
// 深拷贝第一步新建正式Bean独立实例彻底脱离预览Bean的引用
LogUtils.d(TAG, "【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean");
currentBackgroundBean = new BackgroundBean();
// 深拷贝第二步逐字段拷贝预览Bean的所有值压缩图路径同步指向BackgroundCrops
currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName());
currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); // 原图路径BackgroundSource
currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath());
currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo());
currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile());
currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName());
currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径BackgroundCrops
currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态
currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath());
currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile());
currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth());
currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight());
currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor());
// 拷贝一份缓存图片文件到正式背景文件夹
String previewFileName = previewBackgroundBean.getBackgroundFileName();
String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
File previewFile = new File(previewBackgroundBean.getBackgroundFilePath());
@@ -543,42 +435,37 @@ public class BackgroundSourceUtils {
File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName);
FileUtils.copyFile(previewFile, currentFile);
FileUtils.copyFile(previewCropFile, currentCropFile);
// 更新当前背景文件路径
currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath()); // 原图路径BackgroundSource
currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath()); // 压缩图路径BackgroundCrops
currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath());
currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath());
saveSettings(); // 分别保存正式Bean→currentJSON预览Bean→previewJSON两份独立
LogUtils.d(TAG, "【配置管理】预览背景深拷贝到正式Bean两份实例独立压缩图统一存储到BackgroundCrops");
saveSettings();
LogUtils.d(TAG, "预览背景提交到正式背景成功,两份实例完全独立");
ToastUtils.show("背景图片应用成功");
}
/**
* 将正式背景同步到预览背景正式Bean → 预览Bean深拷贝新建预览Bean实例+逐字段拷贝)
* 核心深拷贝后修改预览Bean不会影响正式Bean两份实例完全独立压缩图路径统一指向BackgroundCrops
* 将正式背景同步到预览背景
*/
public void setCurrentSourceToPreview() {
LogUtils.d(TAG, "正在初始化预览数据,setCurrentSourceToPreview()");
// 深拷贝第一步新建预览Bean独立实例彻底脱离正式Bean的引用
LogUtils.d(TAG, "setCurrentSourceToPreview调用】开始深拷贝正式Bean到预览Bean");
previewBackgroundBean = new BackgroundBean();
// 深拷贝第二步逐字段拷贝正式Bean的所有值压缩图路径同步指向BackgroundCrops
previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName());
previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); // 原图路径BackgroundSource
previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath());
previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo());
previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile());
previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径BackgroundCrops
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态
previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath());
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile());
previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth());
previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight());
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
saveSettings();
LogUtils.d(TAG, "正式背景同步到预览背景成功");
}
/**
* 工具方法:清理旧文件(避免文件锁定/残留,适配系统公共目录)【内部私有,不对外暴露】
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
* 清理单个旧文件
*/
private void clearOldFile(File file, String fileDesc) {
if (file == null) {
@@ -586,50 +473,45 @@ public class BackgroundSourceUtils {
}
if (file.exists()) {
file.delete();
LogUtils.w(TAG, "【文件管理】" + fileDesc + "已删除");
LogUtils.d(TAG, fileDesc + "已删除");
}
}
/**
* 新增:清理压缩图目录下的旧文件(避免残留,初始化时调用)
* 清理裁剪临时文件
*/
void clearCropTempFiles() {
for (File file : fCropCacheDir.listFiles()) {
clearOldFile(file, "旧裁剪缓存文件(" + file.getAbsolutePath() + ")");
File[] files = fCropCacheDir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
clearOldFile(file, "旧裁剪缓存文件:" + file.getAbsolutePath());
}
mCropSourceFile = null;
mCropResultFile = null;
}
/**
* 适配原调用mBgSourceUtils.copyFile(new File(""), parentDir)
* 核心复用FileUtils支持「空源文件→仅创建目标目录」和「正常文件复制」两种场景
* @param source 源文件(可为空/空文件,为空时仅创建目标目录)
* @param target 目标文件/目录若源文件为空target视为目录并创建
* @return true=复制/创建成功false=失败
* 复制文件
*/
public boolean copyFile(File source, File target) {
// 场景1源文件为空适配 new File("") 调用)→ 仅创建目标目录
if (source == null || (source.exists() && source.length() <= 0) || TextUtils.isEmpty(source.getPath())) {
LogUtils.d(TAG, "【copyFile调用】源文件" + (source != null ? source.getAbsolutePath() : "null") + " 目标:" + (target != null ? target.getAbsolutePath() : "null"));
if (source == null || TextUtils.isEmpty(source.getPath()) || (source.exists() && source.length() <= 0)) {
if (target == null) {
LogUtils.e(TAG, "【文件管理】目录创建失败:目标目录对象为null");
LogUtils.e(TAG, "目录创建失败目标对象为null");
return false;
}
// 若target是文件取其父目录若本身是目录直接创建实例化时已创建此处二次确认
File targetDir = target.isFile() ? target.getParentFile() : target;
createDirWithPermission(targetDir, "空源文件场景-目录创建/Pictures/PowerBell下");
LogUtils.d(TAG, "【文件管理】空源文件场景目录创建完成,路径=" + targetDir.getAbsolutePath());
createDirWithPermission(targetDir, "空源文件场景-目录创建");
LogUtils.d(TAG, "空源文件场景目录创建完成");
return true;
}
// 场景2正常文件复制源文件非空且存在→ 复用FileUtils.copyFile确保高效兼容
return FileUtils.copyFile(source, target);
}
/**
* 工具方法:获取目录类型描述(用于日志调试,明确目录类型,适配新目录结构)
* @param dir 目标目录
* @return 目录类型描述
* 获取目录类型描述
*/
public String getDirTypeDesc(File dir) {
if (dir == null) {
@@ -642,7 +524,7 @@ public class BackgroundSourceUtils {
if (!TextUtils.isEmpty(publicPicturePath)) {
if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_DIR_NAME)) {
return "系统公共图片目录(/Pictures/PowerBell/BackgroundCrops压缩图统一存储目录"; // 新增压缩图目录描述
return "系统公共图片目录(/Pictures/PowerBell/BackgroundCompress压缩图统一存储目录";
} else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) {
return "系统公共图片目录(/Pictures/PowerBell图片存储/裁剪目录)";
}
@@ -657,86 +539,68 @@ public class BackgroundSourceUtils {
}
/**
* 新增:迁移旧压缩图路径到新目录BackgroundCrops兼容历史数据
* @param bean 要迁移的BackgroundBean正式/预览)
* @param isCurrentBean 是否是正式Bean用于日志区分
* 迁移旧压缩图路径到新目录
*/
private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) {
LogUtils.d(TAG, "【migrateCompressPathToNewDir调用】开始迁移" + (isCurrentBean ? "正式" : "预览") + "Bean压缩路径");
String oldCompressPath = bean.getBackgroundScaledCompressFilePath();
String beanType = isCurrentBean ? "正式Bean" : "预览Bean";
// 校验旧路径非空且不在BackgroundCrops目录下才需要迁移
if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) {
LogUtils.d(TAG, "【路径迁移】" + beanType + "无需迁移:旧路径为空或已在BackgroundCrops目录");
LogUtils.d(TAG, beanType + "无需迁移:旧路径为空或已在目标目录");
return;
}
File oldCompressFile = new File(oldCompressPath);
if (!oldCompressFile.exists() || !oldCompressFile.isFile() || oldCompressFile.length() <= 0) {
LogUtils.w(TAG, "【路径迁移】" + beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath);
// 重置路径为新目录下的空文件(避免无效路径)
LogUtils.w(TAG, beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath);
String compressFileName = bean.getBackgroundScaledCompressFileName();
if (!TextUtils.isEmpty(compressFileName)) {
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
saveSettings();
LogUtils.d(TAG, "【路径迁移】" + beanType + "重置压缩路径到BackgroundCrops" + newCompressFile.getAbsolutePath());
LogUtils.d(TAG, beanType + "压缩路径已重置到目标目录");
}
return;
}
// 迁移逻辑复制旧文件到新目录更新Bean路径删除旧文件
String compressFileName = bean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg"; // 兜底生成文件名
compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg";
}
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
// 复制旧文件到新目录
boolean copySuccess = FileUtils.copyFile(oldCompressFile, newCompressFile);
if (copySuccess) {
// 更新Bean路径为新目录路径
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
saveSettings();
// 删除旧文件(清理残留)
clearOldFile(oldCompressFile, beanType + "旧压缩文件(迁移后清理)");
LogUtils.d(TAG, "【路径迁移】" + beanType + "压缩路径迁移成功:" + oldCompressPath + "" + newCompressFile.getAbsolutePath());
LogUtils.d(TAG, beanType + "压缩路径迁移成功:" + oldCompressPath + "" + newCompressFile.getAbsolutePath());
} else {
LogUtils.e(TAG, "【路径迁移】" + beanType + "压缩文件复制失败,迁移终止" + oldCompressPath);
LogUtils.e(TAG, beanType + "压缩文件复制失败,迁移终止");
}
}
// ======================================== 核心实现:获取图片旋转角度 ========================================
/**
* 取图片EXIF信息获取旋转角度适配JPEG/PNG等主流格式
* @param imagePath 图片绝对路径(支持本地文件路径,兼容多包名临时目录)
* @return 旋转角度0/90/180/270无旋转返回0
* 取图片旋转角度
*/
public int getImageRotateAngle(String imagePath) {
// 1. 入参校验(避免空指针/无效路径)
LogUtils.d(TAG, "【getImageRotateAngle调用】图片路径" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
Log.e(TAG, "getImageRotateAngle: 图片路径为空");
LogUtils.e(TAG, "图片路径为空");
return 0;
}
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
Log.e(TAG, "getImageRotateAngle: 图片文件无效,路径" + imagePath);
LogUtils.e(TAG, "图片文件无效:" + imagePath);
return 0;
}
InputStream inputStream = null;
try {
// 2. 读取图片EXIF信息优先用流读取避免文件占用
inputStream = new FileInputStream(imageFile);
ExifInterface exifInterface = new ExifInterface(inputStream);
// 3. 获取旋转角度标签兼容不同设备的EXIF字段
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
);
// 4. 解析旋转角度标准EXIF角度映射
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
@@ -744,103 +608,83 @@ public class BackgroundSourceUtils {
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default: // 正常/翻转等其他情况均视为0度
default:
return 0;
}
} catch (IOException e) {
// 兼容异常场景如图片无EXIF信息、格式不支持如WebP
Log.w(TAG, "getImageRotateAngle: 读取EXIF异常路径" + imagePath + ",错误:" + e.getMessage());
LogUtils.w(TAG, "读取EXIF异常" + e.getMessage());
return 0;
} finally {
// 5. 关闭流资源(避免内存泄漏/文件占用)
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e(TAG, "getImageRotateAngle: 流关闭失败,错误" + e.getMessage());
LogUtils.e(TAG, "流关闭失败:" + e.getMessage());
}
}
}
}
// ======================================== 图片处理核心方法(压缩/裁剪/保存) ========================================
/**
* 压缩图片并保存(核心修复:路径非空校验+兜底路径统一存储到BackgroundCrops目录
* 压缩图片并保存(默认路径
*/
public void compressQualityToRecivedPicture(Bitmap bitmap) {
// 兼容裁剪等旧调用从工具类获取默认压缩路径统一指向BackgroundCrops转发至重载函数
LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】使用默认路径压缩图片");
String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath();
compressQualityToRecivedPicture(bitmap, defaultCompressPath);
}
/**
* 重载方法指定路径压缩图片并保存修复压缩后同步路径到预览Bean统一存储到BackgroundCrops
* 适配场景裁剪后生成压缩图强制绑定路径到预览Bean避免路径错位
* @param bitmap 待压缩的Bitmap裁剪后的缩放图
* @param targetCompressPath 强制指定的压缩目标路径从预览Bean获取/生成默认指向BackgroundCrops
* 压缩图片并保存(指定路径
*/
public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) {
LogUtils.d(TAG, "压缩启动】开始压缩图片指定路径Bitmap状态" + (bitmap != null && !bitmap.isRecycled()));
LogUtils.d(TAG, "compressQualityToRecivedPicture调用】指定路径压缩图片目标路径" + targetCompressPath);
if (bitmap == null || bitmap.isRecycled()) {
ToastUtils.show("压缩失败:图片为空");
LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收");
LogUtils.e(TAG, "Bitmap为空或已回收");
return;
}
OutputStream outStream = null;
FileOutputStream fos = null;
try {
LogUtils.d(TAG, "【压缩配置】目标路径BackgroundCrops" + targetCompressPath + "Bitmap原始大小" + bitmap.getByteCount() / 1024 + "KB");
LogUtils.d(TAG, "Bitmap原始大小" + bitmap.getByteCount() / 1024 + "KB");
File targetCompressFile = new File(targetCompressPath);
if (targetCompressFile.exists()) {
targetCompressFile.delete();
}
targetCompressFile.createNewFile();
// 写入压缩图质量80平衡清晰度和内存
fos = new FileOutputStream(targetCompressFile);
outStream = new BufferedOutputStream(fos);
boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
outStream.flush();
// 强制同步到磁盘(避免异步写入导致控件读取不到文件)
if (fos != null) {
try {
fos.getFD().sync();
LogUtils.d(TAG, "【压缩保存】已强制同步到磁盘");
LogUtils.d(TAG, "图片已强制同步到磁盘");
} catch (IOException e) {
LogUtils.w(TAG, "【压缩保存】sync()失败flush()兜底:" + e.getMessage());
LogUtils.w(TAG, "sync失败flush兜底" + e.getMessage());
outStream.flush();
}
}
LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB,路径:" + targetCompressFile);
// 关键修复压缩成功后强制同步路径到预览Bean双重保障避免时序错位
if (compressSuccess) {
ToastUtils.show("图片压缩成功");
} else {
ToastUtils.show("图片压缩失败");
}
LogUtils.d(TAG, "图片压缩" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB");
ToastUtils.show(compressSuccess ? "图片压缩成功" : "图片压缩失败");
} catch (IOException e) {
LogUtils.e(TAG, "【压缩异常】IO错误" + e.getMessage(), e);
LogUtils.e(TAG, "图片压缩IO异常" + e.getMessage(), e);
ToastUtils.show("图片压缩失败");
} finally {
// 资源回收(避免内存泄漏)
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【流关闭失败】BufferedOutputStream" + e.getMessage());
LogUtils.e(TAG, "BufferedOutputStream关闭失败" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "【流关闭失败】FileOutputStream" + e.getMessage());
LogUtils.e(TAG, "FileOutputStream关闭失败" + e.getMessage());
}
}
if (bitmap != null && !bitmap.isRecycled()) {
@@ -849,3 +693,4 @@ public class BackgroundSourceUtils {
}
}
}

View File

@@ -0,0 +1,206 @@
package cc.winboll.studio.powerbell.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.TextUtils;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/11 01:57
* @Describe 单例 Bitmap 缓存工具类Java 7 兼容)
* 功能:内存缓存 Bitmap支持路径关联缓存、全局获取、缓存清空
* 特点1. 单例模式 2. 压缩加载避免OOM 3. 路径- Bitmap 映射 4. 线程安全
*/
public class BitmapCacheUtils {
public static final String TAG = "BitmapCacheUtils";
// 最大图片尺寸适配1080P屏幕可根据需求调整
private static final int MAX_WIDTH = 1080;
private static final int MAX_HEIGHT = 1920;
// 单例实例volatile 保证多线程可见性)
private static volatile BitmapCacheUtils sInstance;
// 路径-Bitmap 缓存容器(内存缓存)
private final Map<String, Bitmap> mBitmapCacheMap;
// 私有构造器(单例模式)
private BitmapCacheUtils() {
mBitmapCacheMap = new HashMap<>();
}
/**
* 获取单例实例(双重校验锁,线程安全)
*/
public static BitmapCacheUtils getInstance() {
if (sInstance == null) {
synchronized (BitmapCacheUtils.class) {
if (sInstance == null) {
sInstance = new BitmapCacheUtils();
}
}
}
return sInstance;
}
/**
* 核心接口:根据图片路径缓存 Bitmap 到内存
* @param imagePath 图片绝对路径
* @return 缓存成功的 Bitmap / null路径无效/文件不存在/解码失败)
*/
public Bitmap cacheBitmap(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "cacheBitmap: 图片路径为空");
return null;
}
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "cacheBitmap: 图片文件无效(不存在/非文件/空文件) - " + imagePath);
return null;
}
// 已缓存则直接返回,避免重复加载
if (mBitmapCacheMap.containsKey(imagePath)) {
Bitmap cachedBitmap = mBitmapCacheMap.get(imagePath);
// 额外校验缓存的Bitmap是否有效
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
LogUtils.d(TAG, "cacheBitmap: 图片已缓存,直接返回 - " + imagePath);
return cachedBitmap;
} else {
// 缓存的Bitmap已失效移除后重新加载
mBitmapCacheMap.remove(imagePath);
LogUtils.w(TAG, "cacheBitmap: 缓存Bitmap已失效移除后重新加载 - " + imagePath);
}
}
// 压缩加载 Bitmap避免OOM
Bitmap bitmap = decodeCompressedBitmap(imagePath);
if (bitmap != null) {
// 存入缓存容器
mBitmapCacheMap.put(imagePath, bitmap);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功 - " + imagePath);
} else {
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
}
return bitmap;
}
/**
* 核心接口:根据路径获取缓存的 Bitmap
* @param imagePath 图片绝对路径
* @return 缓存的有效 Bitmap / null未缓存/已回收)
*/
public Bitmap getCachedBitmap(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
return null;
}
Bitmap bitmap = mBitmapCacheMap.get(imagePath);
// 校验Bitmap是否有效
if (bitmap != null && bitmap.isRecycled()) {
mBitmapCacheMap.remove(imagePath);
return null;
}
return bitmap;
}
/**
* 清空所有 Bitmap 缓存(释放内存)
*/
public void clearAllCache() {
synchronized (mBitmapCacheMap) {
for (Bitmap bitmap : mBitmapCacheMap.values()) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle(); // 主动回收 Bitmap
}
}
mBitmapCacheMap.clear();
}
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空");
}
/**
* 移除指定路径的 Bitmap 缓存
* @param imagePath 图片绝对路径
*/
public void removeCachedBitmap(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
return;
}
synchronized (mBitmapCacheMap) {
Bitmap bitmap = mBitmapCacheMap.remove(imagePath);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath);
}
}
}
/**
* 压缩解码 Bitmap按最大尺寸缩放避免OOM
* @param imagePath 图片绝对路径
* @return 解码后的 Bitmap / null文件无效/解码失败)
*/
private Bitmap decodeCompressedBitmap(String imagePath) {
// 前置校验:确保文件有效
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "decodeCompressedBitmap: 文件无效,跳过解码 - " + imagePath);
return null;
}
BitmapFactory.Options options = new BitmapFactory.Options();
// 第一步:只获取图片尺寸,不加载像素
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath, options);
// 校验尺寸是否有效
if (options.outWidth <= 0 || options.outHeight <= 0) {
LogUtils.e(TAG, "decodeCompressedBitmap: 图片尺寸无效 - " + imagePath);
return null;
}
// 计算缩放比例
int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
// 第二步:加载压缩后的 Bitmap
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存比ARGB_8888少一半内存
options.inPurgeable = true;
options.inInputShareable = true;
try {
return BitmapFactory.decodeFile(imagePath, options);
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath);
return null;
} catch (Exception e) {
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e);
return null;
}
}
/**
* 计算 Bitmap 缩放比例
*/
private int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) {
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int inSampleSize = 1;
if (rawWidth > maxWidth || rawHeight > maxHeight) {
int halfWidth = rawWidth / 2;
int halfHeight = rawHeight / 2;
while ((halfWidth / inSampleSize) >= maxWidth && (halfHeight / inSampleSize) >= maxHeight) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}

View File

@@ -1,47 +1,36 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/01 16:05
* @Describe 权限申请工具类(单例)
* 核心特性:
* 1. 适配全Android版本6.0+ 动态权限 / 13+ 兼容)
* 2. 支持多包名场景(无硬编码包名)
* 3. 统一权限校验、申请、回调处理
* 4. 自带用户引导(拒绝权限+不再询问场景)
* 权限申请工具类
* 适配 Android 13+ 媒体权限 & 低版本存储权限
* 兼容 SDK 版本低于 33 的编译环境
*/
public class PermissionUtils {
public static final String TAG = "PermissionUtils";
// 存储权限请求码与Activity保持一致避免冲突
public static final int STORAGE_PERMISSION_REQUEST2 = 100;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
private static final String TAG = "PermissionUtils";
// 存储权限请求码
public static final int REQUEST_STORAGE = 1000;
// 媒体图片权限请求码Android 13+
public static final int REQUEST_READ_MEDIA_IMAGES = 1001;
// 单例实例(双重校验锁,线程安全
// 手动定义 Android 13+ 媒体图片权限常量(解决 SDK 低于 33 无法识别问题
private static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES";
// Android 13 对应的 SDK 版本号(替代 Build.VERSION_CODES.TIRAMISU
private static final int SDK_VERSION_TIRAMISU = 33;
// 单例模式
private static volatile PermissionUtils sInstance;
// 私有构造(禁止外部实例化)
private PermissionUtils() {}
/**
* 获取单例实例(适配多包名,无硬编码)
*/
public static PermissionUtils getInstance() {
if (sInstance == null) {
synchronized (PermissionUtils.class) {
@@ -53,161 +42,92 @@ public class PermissionUtils {
return sInstance;
}
// ======================================== 存储权限核心方法 ========================================
/**
* 检查并请存储权限(统一入口,适配全Android版本
* @param activity 上下文(用于权限申请和弹窗)
* @return true权限已全部获取false需要申请权限
* 检查并请存储权限Android 12及以下
*/
public boolean checkAndRequestStoragePermission(Activity activity) {
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【权限检查】Activity为空或已销毁权限检查失败");
return false;
}
LogUtils.d(TAG, "【权限检查】开始检查存储权限Android版本" + Build.VERSION.SDK_INT);
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE适配所有版本避免READ_MEDIA_IMAGES找不到符号
String[] requiredPermissions = {
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+ 无需申请 READ_EXTERNAL_STORAGE直接返回true
return true;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] permissions = {
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
};
// 筛选未授予的权限
List<String> needPermissions = new ArrayList<>();
for (String permission : requiredPermissions) {
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
needPermissions.add(permission);
}
}
// 无权限需要申请:触发动态申请
if (!needPermissions.isEmpty()) {
String[] permissionsArr = needPermissions.toArray(new String[0]);
ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2);
LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr));
if (ContextCompat.checkSelfPermission(activity, permissions[0]) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(activity, permissions[1]) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, permissions, REQUEST_STORAGE);
return false;
}
// 所有权限已授予
LogUtils.d(TAG, "【权限检查】存储权限已全部获取");
}
return true;
}
/**
* 处理存储权限申请回调统一逻辑无需在Activity中重复编写
* @param activity 上下文
* @param requestCode 请求码匹配STORAGE_PERMISSION_REQUEST
* @param permissions 申请的权限数组
* @param grantResults 权限授予结果数组
* @return true回调已处理false非当前工具类的权限回调
* 检查并请求 媒体图片权限Android 13+
* 兼容 SDK 编译版本低于 33 的情况
*/
public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
// 过滤非存储权限回调
if (requestCode != STORAGE_PERMISSION_REQUEST2) {
public boolean checkAndRequestMediaImagesPermission(Activity activity, int requestCode) {
// 用数值 33 替代 Build.VERSION_CODES.TIRAMISU
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
// 用手动定义的权限常量替代 android.Manifest.permission.READ_MEDIA_IMAGES
if (ContextCompat.checkSelfPermission(activity, READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, new String[]{READ_MEDIA_IMAGES}, requestCode);
return false;
}
LogUtils.d(TAG, "【权限回调】处理存储权限回调requestCode" + requestCode);
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【权限回调】Activity为空或已销毁回调处理终止");
}
// 低版本已通过存储权限覆盖直接返回true
return true;
}
// 校验所有权限是否授予
boolean allGranted = true;
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 全部授予:提示用户重新操作
ToastUtils.show(activity.getString(R.string.permission_grant_success));
LogUtils.d(TAG, "【权限回调】所有存储权限已授予");
/**
* 权限请求结果处理
*/
public void handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_STORAGE:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "存储权限申请成功");
} else {
// 部分/全部拒绝:判断是否勾选“不再询问”
boolean shouldShowRationale = false;
for (String permission : permissions) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
shouldShowRationale = true;
LogUtils.e(TAG, "存储权限申请失败");
}
break;
case REQUEST_READ_MEDIA_IMAGES:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "媒体图片权限申请成功");
} else {
LogUtils.e(TAG, "媒体图片权限申请失败");
}
break;
default:
LogUtils.d(TAG, "未知权限请求码:" + requestCode);
}
}
if (shouldShowRationale) {
// 未勾选“不再询问”:弹窗引导重新申请
showPermissionRationaleDialog(activity);
} else {
// 已勾选“不再询问”:引导用户去设置页开启
showPermissionSettingDialog(activity);
}
LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale);
/**
* 检查是否有管理所有文件权限Android 11+
*/
public boolean checkManageExternalStoragePermission(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return android.os.Environment.isExternalStorageManager();
}
return true;
}
// ======================================== 辅助方法(私有,封装细节) ========================================
/**
* 弹窗:未勾选“不再询问”时,提示用户授予权限
* 请求管理所有文件权限Android 11+
*/
private void showPermissionRationaleDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.permission_title))
.setMessage(activity.getString(R.string.permission_storage_rationale))
.setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 重新申请权限
checkAndRequestStoragePermission(activity);
public void requestManageExternalStoragePermission(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
android.content.Intent intent = new android.content.Intent(
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
);
intent.setData(android.net.Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, requestCode);
} catch (Exception e) {
LogUtils.e(TAG, "请求管理文件权限异常:" + e.getMessage());
}
})
.setNegativeButton(activity.getString(R.string.cancel), null)
.show();
}
/**
* 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限
*/
private void showPermissionSettingDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.permission_denied_title))
.setMessage(activity.getString(R.string.permission_storage_setting_guide))
.setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 跳转应用设置页(适配多包名,动态获取当前包名)
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivity(intent);
}
})
.setNegativeButton(activity.getString(R.string.cancel), null)
.show();
}
// ======================================== 扩展:其他权限方法(可选) ========================================
/**
* 检查单个权限是否已授予(通用方法,可复用)
*/
public boolean isPermissionGranted(Activity activity, String permission) {
if (activity == null || TextUtils.isEmpty(permission)) {
return false;
}
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED;
}
/**
* 申请单个权限(通用方法,可复用)
*/
public void requestSinglePermission(Activity activity, String permission, int requestCode) {
if (activity == null || TextUtils.isEmpty(permission)) {
return;
}
if (!isPermissionGranted(activity, permission)) {
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
}
}
}

View File

@@ -10,8 +10,10 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -105,16 +107,81 @@ public class UriUtil {
}
public static Uri getUriForFile(Context context, String filePath) {
// 1. 打印传入的文件路径
LogUtils.d(TAG, "getUriForFile -> 传入路径:" + filePath);
if (filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "getUriForFile -> 传入路径为空");
return null;
}
File file = new File(filePath);
// 2. 打印File对象的绝对路径和存在性
LogUtils.d(TAG, "getUriForFile -> 文件绝对路径:" + file.getAbsolutePath());
LogUtils.d(TAG, "getUriForFile -> 文件是否存在:" + file.exists());
LogUtils.d(TAG, "getUriForFile -> 是否为目录:" + file.isDirectory());
// 3. 合法性校验
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile -> 非法路径:文件不存在或为目录");
return null;
}
// 4. 校验路径是否在配置的合法目录内
String appFilesDir = context.getExternalFilesDir(null) != null ? context.getExternalFilesDir(null).getAbsolutePath() : "null";
String publicPicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBell/";
String internalFilesDir = context.getFilesDir().getAbsolutePath();
String cacheDir = context.getCacheDir().getAbsolutePath();
String absolutePath = file.getAbsolutePath();
boolean isInConfigDir = absolutePath.startsWith(appFilesDir)
|| absolutePath.startsWith(publicPicDir)
|| absolutePath.startsWith(internalFilesDir)
|| absolutePath.startsWith(cacheDir);
LogUtils.d(TAG, "getUriForFile -> 路径是否在配置目录内:" + isInConfigDir);
if (!isInConfigDir) {
LogUtils.w(TAG, "getUriForFile -> 路径不在FileProvider配置范围内可能导致异常");
// 非强制拦截,保留原有逻辑,仅警告
}
return getUriForFile(context, file);
}
public static Uri getUriForFile(Context context, File file) {
//Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
if (context == null) {
LogUtils.e(TAG, "getUriForFile -> Context为空");
return null;
}
if (file == null) {
LogUtils.e(TAG, "getUriForFile -> File对象为空");
return null;
}
// 1. 二次校验文件状态
LogUtils.d(TAG, "getUriForFile(File) -> 文件路径:" + file.getAbsolutePath());
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile(File) -> 文件不存在或为目录");
return null;
}
// 2. 版本判断与Uri生成
if (Build.VERSION.SDK_INT >= 24) {
LogUtils.d(TAG, "getUriForFile -> Android 7.0+使用FileProvider生成Uri");
try {
String authority = context.getPackageName() + ".fileprovider";
LogUtils.d(TAG, "getUriForFile -> FileProvider authority" + authority);
Uri uri = FileProvider.getUriForFile(context, authority, file);
LogUtils.d(TAG, "getUriForFile -> 生成Content Uri成功" + uri.toString());
return uri;
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "getUriForFile -> FileProvider生成Uri失败路径未配置或权限不足", e);
return null;
}
} else {
LogUtils.d(TAG, "getUriForFile -> Android 7.0以下使用Uri.fromFile生成Uri");
Uri uri = Uri.fromFile(file);
LogUtils.d(TAG, "getUriForFile -> 生成File Uri成功" + uri.toString());
return uri;
}
return Uri.fromFile(file);
}
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)

View File

@@ -12,6 +12,8 @@ import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import java.io.File;
@@ -22,6 +24,8 @@ import java.io.File;
public class BackgroundView extends RelativeLayout {
public static final String TAG = "BackgroundView";
// 新增:记录当前已缓存的图片路径
private String mCurrentCachedPath = "";
private Context mContext;
private LinearLayout mLlContainer; // 主容器LinearLayout
@@ -55,8 +59,6 @@ public class BackgroundView extends RelativeLayout {
LogUtils.d(TAG, "=== initView 启动 ===");
// 1. 配置当前控件:全屏+透明
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
//setBackgroundColor(0x00000000);
//setBackground(new ColorDrawable(0x00000000));
// 2. 初始化主容器LinearLayout
initLinearLayout();
@@ -102,21 +104,34 @@ public class BackgroundView extends RelativeLayout {
}
public void loadBackgroundBean(BackgroundBean bean) {
loadBackgroundBean(bean, false);
}
public void loadBackgroundBean(BackgroundBean bean, boolean isRefresh) {
if (!bean.isUseBackgroundFile()) {
setDefaultTransparentBackground();
return;
}
if (bean.isUseBackgroundScaledCompressFile()) {
loadImage(bean.getBackgroundScaledCompressFilePath());
} else {
String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
loadImage(bean.getBackgroundFilePath());
if (!(new File(targetPath).exists())) {
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
return;
}
// 调用带路径判断的loadImage方法
if (isRefresh) {
App._mBitmapCacheUtils.removeCachedBitmap(targetPath);
App._mBitmapCacheUtils.cacheBitmap(targetPath);
}
loadImage(targetPath);
}
// ====================================== 对外方法 ======================================
/**
* 加载图片保持原图比例在LinearLayout中居中平铺
* 改造后添加路径判断路径更新时同步更新缓存缓存Bitmap为null时提示并加载透明背景
* @param imagePath 图片绝对路径
*/
public void loadImage(String imagePath) {
@@ -135,22 +150,56 @@ public class BackgroundView extends RelativeLayout {
mIvBackground.setVisibility(View.GONE);
// 计算原图比例
// ======================== 新增:路径判断逻辑 ========================
// 1. 路径未变化:直接使用缓存
if (imagePath.equals(mCurrentCachedPath)) {
Bitmap cachedBitmap = App._mBitmapCacheUtils.getCachedBitmap(imagePath);
// 核心修改判断缓存Bitmap是否为null
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
mIvBackground.setVisibility(View.VISIBLE);
return;
} else {
// 缓存Bitmap为空或已回收提示并加载透明背景
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath);
ToastUtils.show("全局位图缓存为空,无法加载图片");
setDefaultTransparentBackground();
return;
}
}
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
App._mBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath);
}
// ======================== 路径判断逻辑结束 ========================
// 无缓存/路径更新:走原有逻辑加载图片
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
}
// 压缩加载Bitmap
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground();
return;
}
// 设置图片
// 缓存新图片,并更新当前缓存路径记录
App._mBitmapCacheUtils.cacheBitmap(imagePath);
mCurrentCachedPath = imagePath;
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
adjustImageViewSize(); // 调整尺寸
adjustImageViewSize();
mIvBackground.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "=== loadImage 完成 ===");
}
@@ -198,30 +247,15 @@ public class BackgroundView extends RelativeLayout {
}
}
/**
* 调整ImageView尺寸保持原图比例在LinearLayout中居中平铺
*/
private void adjustImageViewSize() {
//LogUtils.d(TAG, "=== adjustImageViewSize 启动 ===");
if (mLlContainer == null || mIvBackground == null) {
//LogUtils.e(TAG, "控件为空");
return;
}
// 获取LinearLayout尺寸
int llWidth = mLlContainer.getWidth();
int llHeight = mLlContainer.getHeight();
if (llWidth == 0 || llHeight == 0) {
postDelayed(new Runnable() {
@Override
public void run() {
adjustImageViewSize();
}
}, 100);
return;
}
// 计算ImageView尺寸保持比例不超出LinearLayout
if (llWidth != 0 && llHeight != 0) {
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
@@ -231,23 +265,22 @@ public class BackgroundView extends RelativeLayout {
ivWidth = (int) (ivHeight * mImageAspectRatio);
}
// 应用尺寸
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 确保居中平铺
mIvBackground.setScaleType(ScaleType.FIT_CENTER);
mIvBackground.setVisibility(View.VISIBLE);
//LogUtils.d(TAG, "ImageView尺寸" + ivWidth + "x" + ivHeight);
//LogUtils.d(TAG, "=== adjustImageViewSize 完成 ===");
}
}
private void setDefaultTransparentBackground() {
mIvBackground.setImageBitmap(null);
mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f;
// 清空缓存路径记录
mCurrentCachedPath = "";
mIvBackground.setVisibility(View.GONE);
}
// ====================================== 重写方法 ======================================
@@ -257,3 +290,4 @@ public class BackgroundView extends RelativeLayout {
adjustImageViewSize(); // 尺寸变化时重新调整
}
}

View File

@@ -2,9 +2,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_log"
android:title="@string/item_logview"/>
<item
android:id="@+id/action_unittestactivity"
android:title="@string/item_mainunittestactivity"/>