diff --git a/powerbell/build.properties b/powerbell/build.properties index 4b7b160..3561d62 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Dec 22 15:19:14 GMT 2025 +#Tue Dec 23 04:54:11 GMT 2025 stageCount=24 libraryProject= baseVersion=15.14 publishVersion=15.14.23 -buildCount=3 +buildCount=4 baseBetaVersion=15.14.24 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BatteryReportActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BatteryReportActivity.java index d0b3baa..95a6fca 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BatteryReportActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BatteryReportActivity.java @@ -1,10 +1,5 @@ package cc.winboll.studio.powerbell.activities; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/10/22 13:21 - * @Describe BatteryReportActivity - */ import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; @@ -36,111 +31,87 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +/** + * 电池报告页面,统计应用24小时运行时长与电池消耗情况 + * 支持应用搜索、累计耗电计算、电池广播监听,适配 API30 + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/10/22 13:21 + */ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity { + // ======================== 静态常量 ========================= public static final String TAG = "BatteryReportActivity"; + private static final long ONE_DAY_MS = 24 * 3600 * 1000; // 24小时毫秒数 + private static final long ONE_MINUTE_MS = 60 * 1000; // 1分钟毫秒数 - private Toolbar mToolbar; + // ======================== 成员变量 ========================= + // UI组件 + private Toolbar mToolbar; private RecyclerView rvBatteryReport; + private EditText etSearch; + // 数据与适配器 private BatteryReportAdapter adapter; - private List dataList = new ArrayList(); - private List filteredList = new ArrayList(); + private List dataList = new ArrayList(); + private List filteredList = new ArrayList(); + // 电池相关 private BroadcastReceiver batteryReceiver; private int batteryCapacity = 5400; // 电池容量(mAh) private float lastBatteryPercent = 100.0f; private long lastCheckTime = System.currentTimeMillis(); - private EditText etSearch; + // 缓存相关 private Map appRunTimeCache = new HashMap(); private Map packageToAppNameCache = new HashMap(); private PackageManager mPackageManager; - @Override - public Activity getActivity() { - return this; - } + // ======================== 接口实现方法 ========================= + @Override + public Activity getActivity() { + return this; + } - @Override - public String getTag() { - return TAG; - } + @Override + public String getTag() { + return TAG; + } + // ======================== 生命周期方法 ========================= @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_battery_report); + LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化开始"); - mToolbar = findViewById(R.id.toolbar); - setSupportActionBar(mToolbar); - mToolbar.setSubtitle(getTag()); - mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - mToolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【导航栏】点击返回"); - finish(); - } - }); - + // 初始化UI组件 + initView(); + // 初始化PackageManager mPackageManager = getPackageManager(); // 权限检查(Java7 传统条件判断) if (!hasUsageStatsPermission(this)) { Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show(); startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)); + LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限,引导用户开启"); return; } - etSearch = (EditText) findViewById(R.id.et_search); - rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report); - rvBatteryReport.setLayoutManager(new LinearLayoutManager(this)); - - // 初始化流程:新增“加载24小时累计耗电”步骤 + // 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电 loadAllAppPackage(); preCacheAllAppNames(); appRunTimeCache = getAppRunTime(); updateAppRunTimeToModel(); - calculateInitial24hTotalConsumption(); // 初始化时计算24小时累计耗电 + calculateInitial24hTotalConsumption(); filteredList.addAll(dataList); + + // 初始化适配器 adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache); rvBatteryReport.setAdapter(adapter); + LogUtils.d(TAG, "【onCreate】适配器初始化完成,数据量:" + filteredList.size()); - // 搜索监听(不变) - etSearch.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + // 绑定搜索监听 + bindSearchListener(); + // 注册电池广播 + registerBatteryReceiver(); - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - filterAppsByPackageAndName(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) {} - }); - - // 电池广播:调用修改后的“单次耗电计算+累计累加”方法 - batteryReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - int level = intent.getIntExtra("level", 100); - int scale = intent.getIntExtra("scale", 100); - float currentPercent = (float) level / scale * 100; - LogUtils.d(TAG, "电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent); - - if (currentPercent < lastBatteryPercent) { - float dropPercent = lastBatteryPercent - currentPercent; - long duration = System.currentTimeMillis() - lastCheckTime; - LogUtils.d(TAG, "电池消耗:" + dropPercent + "%,时长:" + duration + "ms"); - appRunTimeCache = getAppRunTime(); - updateAppRunTimeToModel(); - calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache); // 单次+累计逻辑 - } - - lastBatteryPercent = currentPercent; - lastCheckTime = System.currentTimeMillis(); - } - }; - registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成"); } @Override @@ -149,33 +120,133 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc // Java7 显式非空判断 if (batteryReceiver != null) { unregisterReceiver(batteryReceiver); + LogUtils.d(TAG, "【onDestroy】电池广播已注销"); } + LogUtils.d(TAG, "【onDestroy】BatteryReportActivity 销毁完成"); } + // ======================== UI初始化方法 ========================= + private void initView() { + // 初始化Toolbar + mToolbar = findViewById(R.id.toolbar); + setSupportActionBar(mToolbar); + mToolbar.setSubtitle(getTag()); + mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + mToolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【导航栏】点击返回"); + finish(); + } + }); + + // 初始化RecyclerView与搜索框 + etSearch = (EditText) findViewById(R.id.et_search); + rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report); + rvBatteryReport.setLayoutManager(new LinearLayoutManager(this)); + LogUtils.d(TAG, "【initView】UI组件初始化完成"); + } + + // ======================== 搜索监听绑定方法 ========================= + private void bindSearchListener() { + etSearch.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化:" + s.toString()); + filterAppsByPackageAndName(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) {} + }); + LogUtils.d(TAG, "【bindSearchListener】搜索监听绑定完成"); + } + + // ======================== 电池广播注册方法 ========================= + private void registerBatteryReceiver() { + batteryReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int level = intent.getIntExtra("level", 100); + int scale = intent.getIntExtra("scale", 100); + float currentPercent = (float) level / scale * 100; + LogUtils.d(TAG, "【电池广播】电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent); + + if (currentPercent < lastBatteryPercent) { + float dropPercent = lastBatteryPercent - currentPercent; + long duration = System.currentTimeMillis() - lastCheckTime; + LogUtils.d(TAG, "【电池广播】电池消耗:" + dropPercent + "%,时长:" + formatRunTime(duration)); + appRunTimeCache = getAppRunTime(); + updateAppRunTimeToModel(); + calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache); + } + + lastBatteryPercent = currentPercent; + lastCheckTime = System.currentTimeMillis(); + } + }; + registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + LogUtils.d(TAG, "【registerBatteryReceiver】电池广播注册完成"); + } + + // ======================== 权限检查方法 ========================= /** - * 加载所有应用(仅获取包名,初始化模型时单次耗电、累计耗电均设为0) + * 检查是否拥有使用情况访问权限 + * @param context 上下文 + * @return 拥有权限返回true,否则返回false + */ + private boolean hasUsageStatsPermission(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + LogUtils.w(TAG, "【hasUsageStatsPermission】系统版本低于LOLLIPOP,不支持使用情况访问权限"); + return false; + } + + android.app.usage.UsageStatsManager manager = + (android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); + if (manager == null) { + LogUtils.e(TAG, "【hasUsageStatsPermission】获取UsageStatsManager失败"); + return false; + } + + long endTime = System.currentTimeMillis(); + long startTime = endTime - ONE_MINUTE_MS; + List statsList = manager.queryUsageStats( + android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime); + + boolean hasPermission = statsList != null && !statsList.isEmpty(); + LogUtils.d(TAG, "【hasUsageStatsPermission】使用情况访问权限检查结果:" + hasPermission); + return hasPermission; + } + + // ======================== 数据加载与缓存方法 ========================= + /** + * 加载所有应用包名,初始化数据模型 */ private void loadAllAppPackage() { List appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA); dataList.clear(); - LogUtils.d(TAG, "开始加载应用包名列表,共找到" + appList.size() + "个应用"); + LogUtils.d(TAG, "【loadAllAppPackage】开始加载应用包名列表,共找到" + appList.size() + "个应用"); for (ApplicationInfo appInfo : appList) { String packageName = appInfo.packageName; - // 初始化:单次耗电(consumption)=0,累计耗电(totalConsumption)=0,运行时长=0 + // 初始化:单次耗电=0,累计耗电=0,运行时长=0 dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0)); } - LogUtils.d(TAG, "应用包名列表加载完成,共添加" + dataList.size() + "个包名。"); + LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成,共添加" + dataList.size() + "个包名"); } /** - * 预缓存应用名称(逻辑不变) + * 预缓存所有应用名称,减少PackageManager重复调用 */ private void preCacheAllAppNames() { packageToAppNameCache.clear(); - LogUtils.d(TAG, "开始预缓存包名-应用名称映射"); + LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射"); for (AppBatteryModel model : dataList) { String packageName = model.getPackageName(); @@ -183,48 +254,78 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc packageToAppNameCache.put(packageName, appName); } - LogUtils.d(TAG, "预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称"); + LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称"); } /** - * 通过包名获取应用名称(逻辑不变) + * 通过包名获取应用名称,带异常处理 + * @param packageName 应用包名 + * @return 应用名称,获取失败返回包名 */ private String getAppNameByPackage(String packageName) { try { ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0); return mPackageManager.getApplicationLabel(appInfo).toString(); } catch (PackageManager.NameNotFoundException e) { - LogUtils.e(TAG, "包名" + packageName + "对应的应用未找到:" + e.getMessage()); + LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage()); return packageName; } catch (Exception e) { - LogUtils.e(TAG, "查询应用名称失败(包名:" + packageName + "):" + e.getMessage()); + LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "):" + e.getMessage()); return packageName; } } /** - * 更新运行时长到模型(逻辑不变) + * 更新运行时长到数据模型 */ private void updateAppRunTimeToModel() { - int nCount = 0; + int updateCount = 0; for (AppBatteryModel model : dataList) { String packageName = model.getPackageName(); - Long runTime; - if (appRunTimeCache.containsKey(packageName)) { - runTime = appRunTimeCache.get(packageName); - LogUtils.d(TAG, String.format("应用包 %s 运行时长已更新。", packageName)); - nCount++; - } else { - runTime = 0L; - } + Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L; model.setRunTime(runTime); + if (runTime > 0) { + updateCount++; + } } - LogUtils.d(TAG, String.format("dataList.size() %d, appRunTimeCache.size() %d。", dataList.size(), appRunTimeCache.size())); - LogUtils.d(TAG, String.format("updateAppRunTimeToModel() 更新的数据量为:%d", nCount)); + LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成,数据量:" + dataList.size() + ",更新运行时长应用数:" + updateCount); } /** - * 【新增】初始化时计算24小时累计耗电(赋值给totalConsumption) + * 获取应用24小时运行时长 + * @return 应用包名-运行时长(ms)映射 + */ + private Map getAppRunTime() { + Map runTimeMap = new HashMap(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + android.app.usage.UsageStatsManager manager = + (android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); + long endTime = System.currentTimeMillis(); + long startTime = endTime - ONE_DAY_MS; // 近24小时 + List statsList = manager.queryUsageStats( + android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime); + + for (android.app.usage.UsageStats stats : statsList) { + long runTimeMs = stats.getTotalTimeInForeground(); + String packageName = stats.getPackageName(); + runTimeMap.put(packageName, runTimeMs); + LogUtils.v(TAG, "【getAppRunTime】包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs)); + if (packageName.equals("aidepro.top")) { + LogUtils.d(TAG, "【getAppRunTime】特殊查询包名" + packageName + "有结果"); + } + } + } catch (Exception e) { + LogUtils.e(TAG, "【getAppRunTime】获取应用运行时长失败:" + e.getMessage()); + } + } + LogUtils.d(TAG, "【getAppRunTime】应用运行时长列表数量:" + runTimeMap.size()); + return runTimeMap; + } + + // ======================== 核心计算方法 ========================= + /** + * 初始化时计算24小时累计耗电(赋值给totalConsumption) * 逻辑:基于24小时运行时长占比,分配当前电池容量的理论24小时消耗 */ private void calculateInitial24hTotalConsumption() { @@ -233,23 +334,26 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc for (Map.Entry entry : appRunTimeCache.entrySet()) { total24hRunTime += entry.getValue(); } - LogUtils.d(TAG, "24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime)); + LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime)); - // 2. 按运行时长占比分配24小时累计耗电(假设电池满电循环,用总容量近似24小时总消耗) + // 2. 按运行时长占比分配24小时累计耗电 for (AppBatteryModel model : dataList) { String packageName = model.getPackageName(); Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L); // 计算占比与累计耗电 float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0; - float initialTotalConsumption = batteryCapacity * ratio; // 用电池容量近似24小时总消耗 - model.setTotalConsumption(initialTotalConsumption); // 初始化累计耗电 - LogUtils.d(TAG, String.format("应用包 %s 24小时累计耗电初始化:%.1f mAh", packageName, initialTotalConsumption)); + float initialTotalConsumption = batteryCapacity * ratio; + model.setTotalConsumption(initialTotalConsumption); + LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化:" + initialTotalConsumption + " mAh"); } + LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成"); } /** - * 【核心修改】计算单次耗电(赋值给consumption)+ 累加至累计耗电(totalConsumption = totalConsumption + consumption) + * 计算单次耗电(赋值给consumption)+ 累加至累计耗电(totalConsumption = totalConsumption + consumption) + * @param dropPercent 电池下降百分比 + * @param runTimeMap 应用运行时长映射 */ private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map runTimeMap) { long totalSingleRunTime = 0; @@ -257,25 +361,26 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc for (Map.Entry entry : runTimeMap.entrySet()) { totalSingleRunTime += entry.getValue(); } + LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长:" + formatRunTime(totalSingleRunTime)); // 2. 遍历计算每个应用的“单次耗电”并“累加至累计” for (AppBatteryModel model : dataList) { String packageName = model.getPackageName(); Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L); - // 步骤1:计算本次单次耗电(赋值给consumption) + // 步骤1:计算本次单次耗电 float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0; - float singleConsumption = batteryCapacity * dropPercent / 100 * ratio; // 单次消耗 - model.setConsumption(singleConsumption); // 存储单次耗电 + float singleConsumption = batteryCapacity * dropPercent / 100 * ratio; + model.setConsumption(singleConsumption); - // 步骤2:累加单次耗电到累计耗电(totalConsumption = 原有累计 + 本次单次) + // 步骤2:累加单次耗电到累计耗电 float newTotalConsumption = model.getTotalConsumption() + singleConsumption; - model.setTotalConsumption(newTotalConsumption); // 更新累计耗电 + model.setTotalConsumption(newTotalConsumption); // 同步运行时长 model.setRunTime(appSingleRunTime); - LogUtils.d(TAG, String.format("应用包 %s:单次耗电%.1f mAh,累计耗电%.1f mAh", + LogUtils.v(TAG, String.format("【calculateSingleConsumptionAndAccumulate】应用包%s:单次耗电%.1f mAh,累计耗电%.1f mAh", packageName, singleConsumption, newTotalConsumption)); } @@ -289,69 +394,43 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc // 4. 重新应用过滤并刷新列表 filterAppsByPackageAndName(etSearch.getText().toString()); + LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成,列表已刷新"); } /** - * 双维度过滤(逻辑不变) + * 双维度过滤(包名+应用名) + * @param keyword 搜索关键词 */ private void filterAppsByPackageAndName(String keyword) { filteredList.clear(); if (keyword == null || keyword.isEmpty()) { filteredList.addAll(dataList); + LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词为空,显示全部应用,数量:" + filteredList.size()); } else { String lowerKeyword = keyword.toLowerCase(); - for (AppBatteryModel model : dataList) { String packageName = model.getPackageName(); String packageNameLower = packageName.toLowerCase(); String appName = packageToAppNameCache.get(packageName); String appNameLower = appName.toLowerCase(); - boolean isMatched = packageNameLower.contains(lowerKeyword) - || appNameLower.contains(lowerKeyword); + boolean isMatched = packageNameLower.contains(lowerKeyword) + || appNameLower.contains(lowerKeyword); if (isMatched) { filteredList.add(model); } } + LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词:" + keyword + ",匹配应用数量:" + filteredList.size()); } adapter.notifyDataSetChanged(); } + // ======================== 工具方法 ========================= /** - * 获取应用运行时长(逻辑不变,返回24小时运行时长) - */ - private Map getAppRunTime() { - Map runTimeMap = new HashMap(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - try { - android.app.usage.UsageStatsManager manager = - (android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); - long endTime = System.currentTimeMillis(); - long startTime = endTime - 24 * 3600 * 1000; // 近24小时 - List statsList = manager.queryUsageStats( - android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime); - - for (android.app.usage.UsageStats stats : statsList) { - long runTimeMs = stats.getTotalTimeInForeground(); - String packageName = stats.getPackageName(); - LogUtils.d(TAG, "包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs)); - runTimeMap.put(packageName, runTimeMs); - if (packageName.equals("aidepro.top")) { - LogUtils.d(TAG, String.format("runTimeMap.put(packageName, runTimeMs) 特殊查询 %s 查询有结果。", packageName)); - } - } - } catch (Exception e) { - LogUtils.e(TAG, "获取应用运行时长失败:" + e.getMessage()); - } - } - - LogUtils.d(TAG, String.format("应用运行时长列表数量%d。", runTimeMap.size())); - return runTimeMap; - } - - /** - * 格式化运行时长(逻辑不变) + * 格式化运行时长 + * @param runTimeMs 运行时长(ms) + * @return 格式化后的运行时长字符串 */ private String formatRunTime(long runTimeMs) { if (runTimeMs <= 0) { @@ -371,66 +450,47 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc } } + // ======================== 内部类:数据模型 ========================= /** - * 权限检查(逻辑不变) - */ - private boolean hasUsageStatsPermission(Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return false; - } - - android.app.usage.UsageStatsManager manager = - (android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); - if (manager == null) { - return false; - } - - long endTime = System.currentTimeMillis(); - long startTime = endTime - 1000 * 60; - List statsList = manager.queryUsageStats( - android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime); - - return statsList != null && !statsList.isEmpty(); - } - - /** - * 【核心修改】数据模型:明确字段含义 - * - consumption:单次耗电(两次电池广播间的消耗,float类型便于计算) - * - totalConsumption:累计耗电(24小时初始化值+后续单次累加,显示用) + * 应用电池数据模型 + * - consumption:单次耗电(两次电池广播间的消耗) + * - totalConsumption:累计耗电(24小时初始化值+后续单次累加) + * - runTime:运行时长(ms) + * - packageName:应用包名 */ public static class AppBatteryModel { private String packageName; // 应用包名(核心标识) - private float consumption; // 单次耗电(mAh,float类型) - private float totalConsumption;// 累计耗电(mAh,显示+排序用) + private float consumption; // 单次耗电(mAh) + private float totalConsumption;// 累计耗电(mAh) private long runTime; // 运行时长(ms) - // Java7 显式构造:初始化单次耗电、累计耗电为0 + // Java7 显式构造 public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) { this.packageName = packageName; - this.consumption = consumption; // 单次耗电初始为0 - this.totalConsumption = totalConsumption; // 累计耗电初始为0(后续初始化时赋值) + this.consumption = consumption; + this.totalConsumption = totalConsumption; this.runTime = runTime; } - // Getter/Setter:覆盖所有字段,确保数据操作正常 + // Getter/Setter public String getPackageName() { return packageName; } public float getConsumption() { - return consumption; // 获取单次耗电 + return consumption; } public void setConsumption(float consumption) { - this.consumption = consumption; // 设置单次耗电 + this.consumption = consumption; } public float getTotalConsumption() { - return totalConsumption; // 获取累计耗电(显示用) + return totalConsumption; } public void setTotalConsumption(float totalConsumption) { - this.totalConsumption = totalConsumption; // 设置累计耗电(初始化/累加用) + this.totalConsumption = totalConsumption; } public long getRunTime() { @@ -442,8 +502,9 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc } } + // ======================== 内部类:RecyclerView适配器 ========================= /** - * RecyclerView 适配器:仅显示累计耗电(totalConsumption),逻辑适配模型修改 + * 电池报告列表适配器,显示应用名称、累计耗电、运行时长 */ public static class BatteryReportAdapter extends RecyclerView.Adapter { private Context mContext; @@ -451,8 +512,8 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc private PackageManager mPm; private Map mPackageToNameCache; - // Java7 显式构造:接收名称缓存,确保显示时高效获取应用名 - public BatteryReportAdapter(Context context, List dataList, + // Java7 显式构造 + public BatteryReportAdapter(Context context, List dataList, PackageManager pm, Map packageToNameCache) { this.mContext = context; this.mDataList = dataList; @@ -462,18 +523,18 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - // 加载系统列表项布局(text1显示应用名,text2显示累计耗电+时长) View itemView = LayoutInflater.from(mContext) - .inflate(android.R.layout.simple_list_item_2, parent, false); + .inflate(android.R.layout.simple_list_item_2, parent, false); return new ViewHolder(itemView); } @Override public void onBindViewHolder(ViewHolder holder, int position) { - // Java7 显式非空判断:避免空指针异常 + // Java7 显式非空判断 if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) { holder.tvAppName.setText("未知应用"); holder.tvConsumption.setText("累计耗电:0.0 mAh | 运行时长:0秒"); + LogUtils.w(TAG, "【onBindViewHolder】数据异常,位置:" + position); return; } @@ -481,11 +542,11 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc String packageName = model.getPackageName(); String appName = ""; - // 优先从缓存获取应用名:减少PackageManager调用,提升性能 + // 优先从缓存获取应用名 if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) { appName = mPackageToNameCache.get(packageName); } else { - // 缓存无数据时兜底查询,并同步更新缓存 + // 缓存无数据时兜底查询 try { ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0); appName = mPm.getApplicationLabel(appInfo).toString(); @@ -493,45 +554,40 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc mPackageToNameCache.put(packageName, appName); } } catch (PackageManager.NameNotFoundException e) { - appName = packageName; // 包名不存在时用包名兜底 - LogUtils.e("Adapter", "包名" + packageName + "对应的应用未找到:" + e.getMessage()); + appName = packageName; + LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage()); } catch (Exception e) { - appName = packageName; // 其他异常时用包名兜底 - LogUtils.e("Adapter", "查询应用名称失败(包名:" + packageName + "):" + e.getMessage()); + appName = packageName; + LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "):" + e.getMessage()); } } - // 显示逻辑:仅展示累计耗电(totalConsumption),隐藏单次耗电 + // 显示逻辑:应用名称 + 累计耗电 + 运行时长 holder.tvAppName.setText(appName); - // 格式化运行时长 + 累计耗电(保留1位小数,提升可读性) String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime()); String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s", model.getTotalConsumption(), runTimeStr); holder.tvConsumption.setText(totalConsumptionText); - // 显示优化:文字颜色区分(避免所有应用均标蓝,仅示例可按需修改) + // 显示优化 holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black)); holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray)); - - // 调整文字大小:适配手机屏幕,提升可读性 holder.tvAppName.setTextSize(16); holder.tvConsumption.setTextSize(14); } - // 获取列表长度:Java7 三元运算符判断空值,避免空指针 @Override public int getItemCount() { return mDataList == null ? 0 : mDataList.size(); } /** - * ViewHolder:绑定系统布局控件,与显示逻辑对应 + * ViewHolder:绑定系统布局控件 */ public static class ViewHolder extends RecyclerView.ViewHolder { - TextView tvAppName; // 显示应用名称 - TextView tvConsumption; // 显示累计耗电 + 运行时长 + TextView tvAppName; // 应用名称 + TextView tvConsumption; // 累计耗电 + 运行时长 - // Java7 显式构造:绑定控件ID(系统布局固定ID:text1、text2) public ViewHolder(View itemView) { super(itemView); tvAppName = (TextView) itemView.findViewById(android.R.id.text1); diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/ClearRecordActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/ClearRecordActivity.java index 827813a..69970d5 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/ClearRecordActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/ClearRecordActivity.java @@ -6,8 +6,9 @@ import android.os.Bundle; import android.view.View; import android.widget.Switch; import android.widget.TextView; -import cc.winboll.studio.libaes.views.AOHPCTCSeekBar; import androidx.appcompat.widget.Toolbar; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libaes.views.AOHPCTCSeekBar; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.App; @@ -17,39 +18,68 @@ import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver; import cc.winboll.studio.powerbell.utils.AppCacheUtils; import cc.winboll.studio.powerbell.utils.StringUtils; import java.util.ArrayList; -import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +/** + * 电池记录清理页面,支持滑动清理记录、切换记录显示格式 + * 适配 API30,基于 Java7 开发 + * @Author ZhanGSKen&豆包大模型 + */ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity { - + // ======================== 静态常量 ========================= public static final String TAG = "ClearRecordActivity"; + private static final String TOAST_MSG_CLEAR_SUCCESS = "The APP battery record is cleaned."; + // ======================== 成员变量 ========================= + // UI组件 private Toolbar mToolbar; - TextView mtvRecordText; - App mApplication; - boolean mIsShowRecordWithEnter = false; + private TextView mtvRecordText; + private TextView tvAOHPCTCSeekBarMSG; + private AOHPCTCSeekBar aOHPCTCSeekBar; + // 应用与配置 + private App mApplication; + private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示 - @Override - public Activity getActivity() { - return this; - } + // ======================== 接口实现方法 ========================= + @Override + public Activity getActivity() { + return this; + } - @Override - public String getTag() { - return TAG; - } - + @Override + public String getTag() { + return TAG; + } + + // ======================== 生命周期方法 ========================= @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_clearrecord); - mApplication = (App) getApplication(); + LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始"); - // 初始化工具栏 - mToolbar = findViewById(R.id.toolbar); + // 初始化应用实例 + mApplication = (App) getApplication(); + // 初始化UI组件 + initView(); + // 初始化滑动清理控件 + initSeekBar(); + // 初始化记录显示文本 + initRecordText(); + + LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成"); + } + + // ======================== UI初始化方法 ========================= + /** + * 初始化Toolbar与显示文本组件 + */ + private void initView() { + // 初始化Toolbar + mToolbar = findViewById(R.id.toolbar); setSupportActionBar(mToolbar); - mToolbar.setSubtitle(getTag()); + mToolbar.setSubtitle(getTag()); mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); mToolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -58,49 +88,76 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi } }); - // 设置滑动清理控件 - // - // 初始化发送拉动控件 - final AOHPCTCSeekBar aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1); + // 初始化显示文本组件 + tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1); + mtvRecordText = findViewById(R.id.activityclearrecordTextView2); + tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord); + LogUtils.d(TAG, "【initView】UI组件初始化完成"); + } + + /** + * 初始化滑动清理控件 + */ + private void initSeekBar() { + aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1); aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer)); aOHPCTCSeekBar.setThumbOffset(0); - aOHPCTCSeekBar.setOnOHPCListener( - new AOHPCTCSeekBar.OnOHPCListener(){ - - @Override - public void onOHPCommit() { - mApplication.clearBatteryHistory(); - sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION)); - initRecordText(); - String szMSG = "The APP battery record is cleaned."; - LogUtils.d(TAG, szMSG); - ToastUtils.show(szMSG); - } - - - }); - - // 初始化提示框 - TextView tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1); - tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord); - mtvRecordText = findViewById(R.id.activityclearrecordTextView2); - initRecordText(); + aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() { + @Override + public void onOHPCommit() { + LogUtils.d(TAG, "【onOHPCommit】滑动清理触发"); + // 清理电池历史记录 + mApplication.clearBatteryHistory(); + // 发送广播更新前台通知 + sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION)); + // 刷新记录显示 + initRecordText(); + // 提示清理成功 + ToastUtils.show(TOAST_MSG_CLEAR_SUCCESS); + LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成,已发送更新广播"); + } + }); + LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成"); } + // ======================== 业务逻辑方法 ========================= + /** + * 初始化记录显示文本,根据配置切换带换行/不带换行格式 + */ void initRecordText() { ArrayList listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo(); - if (mIsShowRecordWithEnter) { - String szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo); - mtvRecordText.setText(szRecordText); - } else { - String szRecordText = StringUtils.formatPCMListString(listBatteryInfo); - mtvRecordText.setText(szRecordText); - } + String szRecordText; + + // 判空处理:避免空列表导致异常 + if (listBatteryInfo == null || listBatteryInfo.isEmpty()) { + szRecordText = getString(R.string.msg_no_battery_record); + LogUtils.d(TAG, "【initRecordText】无电池记录数据"); + } else { + // 根据配置切换显示格式 + if (mIsShowRecordWithEnter) { + szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo); + LogUtils.d(TAG, "【initRecordText】使用带换行格式显示记录,数量:" + listBatteryInfo.size()); + } else { + szRecordText = StringUtils.formatPCMListString(listBatteryInfo); + LogUtils.d(TAG, "【initRecordText】使用无换行格式显示记录,数量:" + listBatteryInfo.size()); + } + } + + mtvRecordText.setText(szRecordText); + LogUtils.d(TAG, "【initRecordText】记录显示文本初始化完成"); } - public void onShowRecordWithEnter(View view) { - Switch swShowRecordWithEnter = (Switch)view; - mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked(); - initRecordText(); - } + // ======================== 事件回调方法 ========================= + /** + * 切换记录显示格式(带换行/不带换行) + * @param view 触发事件的Switch控件 + */ + public void onShowRecordWithEnter(View view) { + Switch swShowRecordWithEnter = (Switch) view; + mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked(); + LogUtils.d(TAG, "【onShowRecordWithEnter】记录显示格式切换,带换行:" + mIsShowRecordWithEnter); + // 刷新记录显示 + initRecordText(); + } } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java index 1b43f20..cac7353 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -1,7 +1,6 @@ package cc.winboll.studio.powerbell.utils; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.Bitmap; import android.media.ExifInterface; import android.net.Uri; @@ -12,7 +11,6 @@ import androidx.core.content.FileProvider; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.BuildConfig; -import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.models.BackgroundBean; import java.io.BufferedOutputStream; import java.io.File; @@ -30,44 +28,58 @@ import java.util.UUID; */ 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"; + // FileProvider 授权常量 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"; + // 目录名称常量 + private static final String CROP_CACHE_DIR_NAME = "cache"; private static final String SOURCE_DIR_NAME = "BackgroundSource"; private static final String COMPRESS_DIR_NAME = "BackgroundCompress"; + private static final String MODEL_DIR_NAME = "ModelDir"; + // 文件名称常量 + private static final String CURRENT_BEAN_FILE_NAME = "currentBackgroundBean.json"; + private static final String PREVIEW_BEAN_FILE_NAME = "previewBackgroundBean.json"; + private static final String BLANK_ASSET_PATH = "images/blank100x100.png"; + // 图片操作基础目录 + private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell"; + // 压缩常量 + private static final int BITMAP_COMPRESS_QUALITY = 80; + private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG; - // 单例相关 + // ====================== 成员变量(按依赖优先级+功能分类)====================== + // 单例实例 private static volatile BackgroundSourceUtils sInstance; + // 上下文(应用级,避免内存泄漏) private Context mContext; + // 配置文件对象 private File currentBackgroundBeanFile; - private BackgroundBean currentBackgroundBean; private File previewBackgroundBeanFile; + // Bean实例 + private BackgroundBean currentBackgroundBean; private BackgroundBean previewBackgroundBean; - - // 目录文件相关 + // 目录对象 private File fPictureBaseDir; private File fCropCacheDir; private File fBackgroundSourceDir; private File fBackgroundCompressDir; private File fUtilsDir; private File fModelDir; + // 裁剪文件对象 private File mCropSourceFile; private File mCropResultFile; - // 双重校验锁单例 + // ====================== 单例方法(双重校验锁)====================== private BackgroundSourceUtils(Context context) { if (sInstance != null) { throw new RuntimeException("BackgroundSourceUtils 是单例类,禁止重复创建!"); } this.mContext = context.getApplicationContext(); + LogUtils.d(TAG, "【单例初始化】开始初始化必要资源"); initNecessaryDirs(); initAllFiles(); loadSettings(); + LogUtils.d(TAG, "【单例初始化】资源初始化完成"); } public static BackgroundSourceUtils getInstance(Context context) { @@ -81,6 +93,7 @@ public class BackgroundSourceUtils { return sInstance; } + // ====================== 生命周期方法(初始化→加载→保存)====================== /** * 统一初始化所有必要目录 */ @@ -117,13 +130,60 @@ public class BackgroundSourceUtils { LogUtils.e(TAG, "应用外置存储不可用,切换到应用内部缓存目录"); fUtilsDir = mContext.getDataDir(); } - fModelDir = new File(fUtilsDir, "ModelDir"); + fModelDir = new File(fUtilsDir, MODEL_DIR_NAME); createDirWithPermission(fModelDir, "JSON配置目录"); - currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json"); - previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json"); + currentBackgroundBeanFile = new File(fModelDir, CURRENT_BEAN_FILE_NAME); + previewBackgroundBeanFile = new File(fModelDir, PREVIEW_BEAN_FILE_NAME); + LogUtils.d(TAG, "【配置文件初始化】当前Bean文件:" + currentBackgroundBeanFile.getAbsolutePath()); + LogUtils.d(TAG, "【配置文件初始化】预览Bean文件:" + previewBackgroundBeanFile.getAbsolutePath()); } + /** + * 初始化所有文件 + */ + private void initAllFiles() { + clearCropTempFiles(); + LogUtils.d(TAG, "【文件初始化】裁剪临时文件已清理"); + } + + /** + * 加载背景配置 + */ + public void loadSettings() { + LogUtils.d(TAG, "【配置加载】开始加载背景配置"); + // 加载当前Bean + currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); + if (currentBackgroundBean == null) { + currentBackgroundBean = new BackgroundBean(); + BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); + LogUtils.d(TAG, "【配置加载】正式背景Bean不存在,已创建新实例"); + } + // 加载预览Bean + previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); + if (previewBackgroundBean == null) { + previewBackgroundBean = new BackgroundBean(); + BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); + LogUtils.d(TAG, "【配置加载】预览背景Bean不存在,已创建新实例"); + } + LogUtils.d(TAG, "【配置加载】背景配置加载完成"); + } + + /** + * 保存配置 + */ + public void saveSettings() { + LogUtils.d(TAG, "【配置保存】开始保存背景配置"); + if (currentBackgroundBean == null || previewBackgroundBean == null) { + LogUtils.e(TAG, "【配置保存】失败:current/preview Bean存在空值"); + return; + } + BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); + BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); + LogUtils.d(TAG, "【配置保存】两份背景配置保存成功"); + } + + // ====================== 工具方法(目录操作→文件操作→Uri转换→图片处理)====================== /** * 创建目录并校验 */ @@ -136,6 +196,9 @@ public class BackgroundSourceUtils { LogUtils.d(TAG, dirDesc + "不存在,开始创建:" + dir.getAbsolutePath()); dir.mkdirs(); } + if (!dir.exists()) { + LogUtils.e(TAG, dirDesc + "创建失败:mkdirs返回false"); + } } /** @@ -145,339 +208,12 @@ public class BackgroundSourceUtils { 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() { - clearCropTempFiles(); - LogUtils.d(TAG, "文件初始化完成"); - } - - /** - * 将File转为ContentUri - */ - public Uri getFileProviderUri(File file) { - LogUtils.d(TAG, "【getFileProviderUri调用】文件路径:" + file.getAbsolutePath()); - Uri contentUri = null; - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - contentUri = FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITY, file); - LogUtils.d(TAG, "7.0+ 生成ContentUri:" + contentUri.toString()); - } else { - contentUri = Uri.fromFile(file); - LogUtils.d(TAG, "7.0以下 生成FileUri:" + contentUri.toString()); - } - } catch (IllegalArgumentException e) { - LogUtils.e(TAG, "生成Uri失败:" + e.getMessage(), e); - contentUri = null; - } - return contentUri; - } - - /** - * 检查背景是否为空并创建空白背景Bean - */ - public boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) { - LogUtils.d(TAG, "【checkEmptyBackgroundAndCreateBlankBackgroundBean调用】开始检查背景Bean"); - File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath()); - if (!fCheckBackgroundFile.exists()) { - return createBlankBackgroundBean(checkBackgroundBean.getPixelColor()); - } - LogUtils.d(TAG, "背景Bean文件存在,无需创建空白背景"); - return false; - } - - public boolean createBlankBackgroundBean(int nBackgroundPixelColor) { - String newCropFileName = genNewCropFileName(); - String fileSuffix = "png"; - mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); - mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); - - AssetsCopyUtils.copyAssetsFileToDir(mContext, "images/blank100x100.png", mCropSourceFile.getAbsolutePath()); - try { - mCropResultFile.createNewFile(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - - loadSettings(); - previewBackgroundBean.setPixelColor(nBackgroundPixelColor); - 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; - } - - String genNewCropFileName() { - return UUID.randomUUID().toString() + System.currentTimeMillis(); - } - - /** - * 创建并更新预览剪裁环境 - */ - 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 = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath()); - LogUtils.d(TAG, String.format("createAndUpdatePreviewEnvironmentForCropping: uri %s", uri)); - String fileSuffix = UriUtils.getSuffixFromUri(mContext, uri); - LogUtils.d(TAG, String.format("createAndUpdatePreviewEnvironmentForCropping: fileSuffix = %s", fileSuffix)); - String newCropFileName = genNewCropFileName(); - mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); - mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + ".png"); - - if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) { - FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile); - } else { - mCropResultFile.createNewFile(); - } - - if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundFilePath())) { - FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile); - } else { - mCropSourceFile.createNewFile(); - is = mContext.getContentResolver().openInputStream(uri); - if (is == null) { - LogUtils.e(TAG, "ContentResolver打开Uri失败:" + uri.toString()); - return false; - } - fos = new FileOutputStream(mCropSourceFile); - byte[] buffer = new byte[1024 * 8]; - int readLen; - while ((readLen = is.read(buffer)) != -1) { - fos.write(buffer, 0, readLen); - } - fos.flush(); - try { - fos.getFD().sync(); - } catch (IOException e) { - 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(); - - LogUtils.d(TAG, "预览剪裁环境初始化成功"); - LogUtils.d(TAG, "→ 原Uri:" + uri.toString()); - LogUtils.d(TAG, "→ 剪裁数据源:" + mCropSourceFile.getAbsolutePath()); - LogUtils.d(TAG, "→ 剪裁结果文件:" + mCropResultFile.getAbsolutePath()); - return true; - } catch (Exception e) { - LogUtils.e(TAG, "预览剪裁环境初始化异常:" + e.getMessage(), e); - clearCropTempFiles(); - return false; - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage()); - } - } - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage()); - } - } - } - } - - /** - * 加载背景配置 - */ - public void loadSettings() { - currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); - if (currentBackgroundBean == null) { - currentBackgroundBean = new BackgroundBean(); - BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); - LogUtils.d(TAG, "正式背景Bean不存在,已创建新实例"); - } - - previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); - if (previewBackgroundBean == null) { - previewBackgroundBean = new BackgroundBean(); - BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); - LogUtils.d(TAG, "预览背景Bean不存在,已创建新实例"); - } - } - - // ------------------------------ 对外提供的核心方法 ------------------------------ - public BackgroundBean getCurrentBackgroundBean() { - return currentBackgroundBean; - } - - public BackgroundBean getPreviewBackgroundBean() { - return previewBackgroundBean; - } - - public String getPreviewBackgroundScaledCompressFilePath() { - String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); - if (TextUtils.isEmpty(compressFileName)) { - LogUtils.e(TAG, "预览压缩背景文件名为空"); - return ""; - } - File file = new File(fBackgroundCompressDir, compressFileName); - return file.getAbsolutePath(); - } - - public String getCurrentBackgroundScaledCompressFilePath() { - String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName(); - if (TextUtils.isEmpty(compressFileName)) { - LogUtils.e(TAG, "正式压缩背景文件名为空"); - return ""; - } - File file = new File(fBackgroundCompressDir, compressFileName); - return file.getAbsolutePath(); - } - - /** - * 保存配置 - */ - public void saveSettings() { - if (currentBackgroundBean != null && previewBackgroundBean != null) { - BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); - BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); - LogUtils.d(TAG, "两份背景配置保存成功"); - } else { - LogUtils.e(TAG, "配置保存失败:current/preview Bean存在空值"); - } - } - - public String getBackgroundSourceDirPath() { - return fBackgroundSourceDir.getAbsolutePath(); - } - - public String getBackgroundCompressDirPath() { - return fBackgroundCompressDir.getAbsolutePath(); - } - - public String getCropCacheDir() { - return fCropCacheDir.getAbsolutePath(); - } - - public String getFileProviderAuthority() { - return FILE_PROVIDER_AUTHORITY; - } - - // ------------------------------ 核心业务方法 ------------------------------ - /** - * 保存裁剪结果图到预览Bean - */ - public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) { - LogUtils.d(TAG, "【saveFileToPreviewBean调用】源文件路径:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); - if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) { - LogUtils.e(TAG, "源文件无效,拒绝保存"); - return previewBackgroundBean; - } - - String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); - if (sourceFile.getAbsolutePath().contains(originalImageDir)) { - LogUtils.w(TAG, "禁止复制原图,跳过保存"); - return previewBackgroundBean; - } - - 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)) { - LogUtils.d(TAG, "裁剪结果图保存成功:" + targetFile.getAbsolutePath()); - previewBackgroundBean.setBackgroundFileName(uniqueFileName); - previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath()); - previewBackgroundBean.setBackgroundFileInfo(fileInfo); - previewBackgroundBean.setIsUseBackgroundFile(true); - saveSettings(); - } else { - LogUtils.e(TAG, "裁剪结果图复制失败"); - } - return previewBackgroundBean; - } - - /** - * 提交预览背景到正式背景 - */ - public void commitPreviewSourceToCurrent() { - LogUtils.d(TAG, "【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean"); - //ToastUtils.show("【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean"); - currentBackgroundBean = new BackgroundBean(); - currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName()); - currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); - currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo()); - currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile()); - currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName()); - 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()); - File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath()); - File currentFile = new File(fBackgroundSourceDir, previewFileName); - File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName); - FileUtils.copyFile(previewFile, currentFile); - FileUtils.copyFile(previewCropFile, currentCropFile); - currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath()); - currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath()); - - saveSettings(); - LogUtils.d(TAG, "预览背景提交到正式背景成功,两份实例完全独立"); - ToastUtils.show("背景图片应用成功"); - } - - /** - * 将正式背景同步到预览背景 - */ - public void setCurrentSourceToPreview() { - LogUtils.d(TAG, "【setCurrentSourceToPreview调用】开始深拷贝正式Bean到预览Bean"); - previewBackgroundBean = new BackgroundBean(); - previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName()); - previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); - previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo()); - previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile()); - previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName()); - previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); - previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); - previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth()); - previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight()); - previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); - - saveSettings(); - LogUtils.d(TAG, "正式背景同步到预览背景成功"); - } - /** * 清理单个旧文件 */ @@ -486,42 +222,61 @@ public class BackgroundSourceUtils { return; } if (file.exists()) { - file.delete(); - LogUtils.d(TAG, fileDesc + "已删除"); + boolean isDeleted = file.delete(); + LogUtils.d(TAG, fileDesc + (isDeleted ? "已删除" : "删除失败") + ":" + file.getAbsolutePath()); } } /** - * 清理裁剪临时文件 + * 生成新的裁剪文件名称 */ - void clearCropTempFiles() { - File[] files = fCropCacheDir.listFiles(); - if (files == null) { - return; - } - for (File file : files) { - clearOldFile(file, "旧裁剪缓存文件:" + file.getAbsolutePath()); - } - mCropSourceFile = null; - mCropResultFile = null; + String genNewCropFileName() { + String fileName = UUID.randomUUID().toString() + System.currentTimeMillis(); + LogUtils.d(TAG, "【文件命名】生成新裁剪文件名:" + fileName); + return fileName; } /** - * 复制文件 + * 将File转为ContentUri */ - public boolean copyFile(File source, File target) { - 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"); - return false; + public Uri getFileProviderUri(File file) { + LogUtils.d(TAG, "【Uri转换】开始生成FileProvider Uri,文件路径:" + (file != null ? file.getAbsolutePath() : "null")); + if (file == null || !file.exists()) { + LogUtils.e(TAG, "【Uri转换】失败:文件为空或不存在"); + return null; + } + try { + Uri contentUri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + contentUri = FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITY, file); + LogUtils.d(TAG, "【Uri转换】7.0+ 生成ContentUri:" + contentUri.toString()); + } else { + contentUri = Uri.fromFile(file); + LogUtils.d(TAG, "【Uri转换】7.0以下 生成FileUri:" + contentUri.toString()); } - File targetDir = target.isFile() ? target.getParentFile() : target; - createDirWithPermission(targetDir, "空源文件场景-目录创建"); - LogUtils.d(TAG, "空源文件场景,目录创建完成"); - return true; + return contentUri; + } catch (IllegalArgumentException e) { + LogUtils.e(TAG, "【Uri转换】失败:" + e.getMessage(), e); + return null; } - return FileUtils.copyFile(source, target); + } + + /** + * 检查背景是否为空并创建空白背景Bean + */ + public boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) { + LogUtils.d(TAG, "【空白背景检查】开始检查背景Bean"); + if (checkBackgroundBean == null) { + LogUtils.e(TAG, "【空白背景检查】失败:检查Bean为空"); + return false; + } + File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath()); + if (fCheckBackgroundFile.exists()) { + LogUtils.d(TAG, "【空白背景检查】背景Bean文件存在,无需创建空白背景"); + return false; + } + LogUtils.d(TAG, "【空白背景检查】背景Bean文件不存在,开始创建空白背景"); + return createBlankBackgroundBean(checkBackgroundBean.getPixelColor()); } /** @@ -552,28 +307,339 @@ public class BackgroundSourceUtils { return "未知目录"; } + /** + * 获取图片旋转角度 + */ + public int getImageRotateAngle(String imagePath) { + LogUtils.d(TAG, "【图片旋转角度】开始获取图片旋转角度,路径:" + imagePath); + if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "【图片旋转角度】失败:图片路径为空"); + return 0; + } + File imageFile = new File(imagePath); + if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { + LogUtils.e(TAG, "【图片旋转角度】失败:图片文件无效:" + imagePath); + return 0; + } + + InputStream inputStream = null; + try { + inputStream = new FileInputStream(imageFile); + ExifInterface exifInterface = new ExifInterface(inputStream); + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + LogUtils.d(TAG, "【图片旋转角度】90度"); + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + LogUtils.d(TAG, "【图片旋转角度】180度"); + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + LogUtils.d(TAG, "【图片旋转角度】270度"); + return 270; + default: + LogUtils.d(TAG, "【图片旋转角度】0度(正常)"); + return 0; + } + } catch (IOException e) { + LogUtils.w(TAG, "【图片旋转角度】读取EXIF异常:" + e.getMessage()); + return 0; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【图片旋转角度】流关闭失败:" + e.getMessage()); + } + } + } + } + + // ====================== 核心业务方法(按功能分类)====================== + /** + * 创建空白背景Bean + */ + public boolean createBlankBackgroundBean(int nBackgroundPixelColor) { + LogUtils.d(TAG, "【空白背景创建】开始创建空白背景,像素颜色:" + String.format("#%08X", nBackgroundPixelColor)); + String newCropFileName = genNewCropFileName(); + String fileSuffix = "png"; + mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); + mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); + + // 复制空白图片资源 + AssetsCopyUtils.copyAssetsFileToDir(mContext, BLANK_ASSET_PATH, mCropSourceFile.getAbsolutePath()); + LogUtils.d(TAG, "【空白背景创建】空白图片已复制到:" + mCropSourceFile.getAbsolutePath()); + + // 创建结果文件 + try { + mCropResultFile.createNewFile(); + LogUtils.d(TAG, "【空白背景创建】结果文件已创建:" + mCropResultFile.getAbsolutePath()); + } catch (IOException e) { + LogUtils.e(TAG, "【空白背景创建】结果文件创建失败:" + e.getMessage()); + return false; + } + + // 更新预览Bean + loadSettings(); + previewBackgroundBean.setPixelColor(nBackgroundPixelColor); + 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, "【空白背景创建】空白背景创建成功并更新配置"); + return true; + } + + /** + * 创建并更新预览剪裁环境 + */ + public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) { + LogUtils.d(TAG, "【预览剪裁环境】开始初始化预览剪裁环境"); + if (oldPreviewBackgroundBean == null) { + LogUtils.e(TAG, "【预览剪裁环境】失败:旧预览Bean为空"); + return false; + } + + InputStream is = null; + FileOutputStream fos = null; + try { + clearCropTempFiles(); + // 检查并创建空白背景 + if (checkEmptyBackgroundAndCreateBlankBackgroundBean(oldPreviewBackgroundBean)) { + LogUtils.d(TAG, "【预览剪裁环境】空白背景创建成功,直接返回"); + return true; + } + + // 获取Uri和文件后缀 + Uri uri = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath()); + LogUtils.d(TAG, "【预览剪裁环境】原Uri:" + uri); + String fileSuffix = UriUtils.getSuffixFromUri(mContext, uri); + LogUtils.d(TAG, "【预览剪裁环境】文件后缀:" + fileSuffix); + + // 初始化裁剪文件 + String newCropFileName = genNewCropFileName(); + mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); + mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + ".png"); + LogUtils.d(TAG, "【预览剪裁环境】裁剪数据源:" + mCropSourceFile.getAbsolutePath()); + LogUtils.d(TAG, "【预览剪裁环境】裁剪结果文件:" + mCropResultFile.getAbsolutePath()); + + // 复制压缩文件 + if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) { + FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile); + LogUtils.d(TAG, "【预览剪裁环境】已复制旧压缩文件"); + } else { + mCropResultFile.createNewFile(); + LogUtils.d(TAG, "【预览剪裁环境】旧压缩文件不存在,已创建新文件"); + } + + // 复制源文件 + if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundFilePath())) { + FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile); + LogUtils.d(TAG, "【预览剪裁环境】已复制旧源文件"); + } else { + mCropSourceFile.createNewFile(); + is = mContext.getContentResolver().openInputStream(uri); + if (is == null) { + LogUtils.e(TAG, "【预览剪裁环境】ContentResolver打开Uri失败:" + uri.toString()); + return false; + } + fos = new FileOutputStream(mCropSourceFile); + byte[] buffer = new byte[1024 * 8]; + int readLen; + while ((readLen = is.read(buffer)) != -1) { + fos.write(buffer, 0, readLen); + } + fos.flush(); + try { + fos.getFD().sync(); + } catch (IOException e) { + LogUtils.w(TAG, "【预览剪裁环境】文件同步到磁盘失败,flush兜底:" + e.getMessage()); + fos.flush(); + } + LogUtils.d(TAG, "【预览剪裁环境】已从Uri读取并写入源文件"); + } + + // 更新预览Bean + loadSettings(); + previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName()); + previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath()); + previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); + previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); + saveSettings(); + + LogUtils.d(TAG, "【预览剪裁环境】预览剪裁环境初始化成功"); + return true; + } catch (Exception e) { + LogUtils.e(TAG, "【预览剪裁环境】初始化异常:" + e.getMessage(), e); + clearCropTempFiles(); + return false; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【预览剪裁环境】输入流关闭失败:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【预览剪裁环境】输出流关闭失败:" + e.getMessage()); + } + } + } + } + + /** + * 保存裁剪结果图到预览Bean + */ + public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) { + LogUtils.d(TAG, "【裁剪结果保存】开始保存裁剪结果到预览Bean,源文件路径:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); + if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) { + LogUtils.e(TAG, "【裁剪结果保存】失败:源文件无效"); + return previewBackgroundBean; + } + + // 检查是否为原图目录 + String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); + if (sourceFile.getAbsolutePath().contains(originalImageDir)) { + LogUtils.w(TAG, "【裁剪结果保存】禁止复制原图,跳过保存"); + return previewBackgroundBean; + } + + // 确保目录存在 + 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)) { + LogUtils.d(TAG, "【裁剪结果保存】裁剪结果图保存成功:" + targetFile.getAbsolutePath()); + // 更新预览Bean + previewBackgroundBean.setBackgroundFileName(uniqueFileName); + previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath()); + previewBackgroundBean.setBackgroundFileInfo(fileInfo); + previewBackgroundBean.setIsUseBackgroundFile(true); + saveSettings(); + } else { + LogUtils.e(TAG, "【裁剪结果保存】失败:裁剪结果图复制失败"); + } + return previewBackgroundBean; + } + + /** + * 提交预览背景到正式背景 + */ + public void commitPreviewSourceToCurrent() { + LogUtils.d(TAG, "【背景提交】开始深拷贝预览Bean到正式Bean"); + // 深拷贝Bean属性 + currentBackgroundBean = new BackgroundBean(); + copyBackgroundBeanProperties(previewBackgroundBean, currentBackgroundBean); + + // 复制文件 + String previewFileName = previewBackgroundBean.getBackgroundFileName(); + String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); + File previewFile = new File(previewBackgroundBean.getBackgroundFilePath()); + File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath()); + File currentFile = new File(fBackgroundSourceDir, previewFileName); + File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName); + FileUtils.copyFile(previewFile, currentFile); + FileUtils.copyFile(previewCropFile, currentCropFile); + + // 更新文件路径 + currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath()); + currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath()); + + saveSettings(); + LogUtils.d(TAG, "【背景提交】预览背景提交到正式背景成功,两份实例完全独立"); + ToastUtils.show("背景图片应用成功"); + } + + /** + * 将正式背景同步到预览背景 + */ + public void setCurrentSourceToPreview() { + LogUtils.d(TAG, "【背景同步】开始深拷贝正式Bean到预览Bean"); + // 深拷贝Bean属性 + previewBackgroundBean = new BackgroundBean(); + copyBackgroundBeanProperties(currentBackgroundBean, previewBackgroundBean); + + saveSettings(); + LogUtils.d(TAG, "【背景同步】正式背景同步到预览背景成功"); + } + + /** + * 清理裁剪临时文件 + */ + void clearCropTempFiles() { + LogUtils.d(TAG, "【裁剪文件清理】开始清理裁剪临时文件"); + File[] files = fCropCacheDir.listFiles(); + if (files == null) { + LogUtils.d(TAG, "【裁剪文件清理】裁剪缓存目录为空,无需清理"); + return; + } + for (File file : files) { + clearOldFile(file, "旧裁剪缓存文件"); + } + mCropSourceFile = null; + mCropResultFile = null; + LogUtils.d(TAG, "【裁剪文件清理】裁剪临时文件清理完成"); + } + + /** + * 复制文件 + */ + public boolean copyFile(File source, File target) { + LogUtils.d(TAG, "【文件复制】开始复制文件,源文件:" + (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"); + return false; + } + File targetDir = target.isFile() ? target.getParentFile() : target; + createDirWithPermission(targetDir, "空源文件场景-目录创建"); + LogUtils.d(TAG, "【文件复制】空源文件场景,目录创建完成"); + return true; + } + boolean isSuccess = FileUtils.copyFile(source, target); + LogUtils.d(TAG, "【文件复制】" + (isSuccess ? "成功" : "失败")); + return isSuccess; + } + /** * 迁移旧压缩图路径到新目录 */ private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) { - LogUtils.d(TAG, "【migrateCompressPathToNewDir调用】开始迁移" + (isCurrentBean ? "正式" : "预览") + "Bean压缩路径"); + LogUtils.d(TAG, "【路径迁移】开始迁移" + (isCurrentBean ? "正式" : "预览") + "Bean压缩路径"); + if (bean == null) { + LogUtils.e(TAG, "【路径迁移】失败:Bean为空"); + return; + } String oldCompressPath = bean.getBackgroundScaledCompressFilePath(); String beanType = isCurrentBean ? "正式Bean" : "预览Bean"; if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) { - LogUtils.d(TAG, beanType + "无需迁移:旧路径为空或已在目标目录"); + 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 + "压缩路径已重置到目标目录"); + LogUtils.d(TAG, "【路径迁移】" + beanType + "压缩路径已重置到目标目录"); } return; } @@ -589,53 +655,9 @@ public class BackgroundSourceUtils { 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 + "压缩文件复制失败,迁移终止"); - } - } - - /** - * 获取图片旋转角度 - */ - public int getImageRotateAngle(String imagePath) { - LogUtils.d(TAG, "【getImageRotateAngle调用】图片路径:" + imagePath); - if (TextUtils.isEmpty(imagePath)) { - LogUtils.e(TAG, "图片路径为空"); - return 0; - } - File imageFile = new File(imagePath); - if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { - LogUtils.e(TAG, "图片文件无效:" + imagePath); - return 0; - } - - InputStream inputStream = null; - try { - inputStream = new FileInputStream(imageFile); - ExifInterface exifInterface = new ExifInterface(inputStream); - int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - switch (orientation) { - case ExifInterface.ORIENTATION_ROTATE_90: - return 90; - case ExifInterface.ORIENTATION_ROTATE_180: - return 180; - case ExifInterface.ORIENTATION_ROTATE_270: - return 270; - default: - return 0; - } - } catch (IOException e) { - LogUtils.w(TAG, "读取EXIF异常:" + e.getMessage()); - return 0; - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - LogUtils.e(TAG, "流关闭失败:" + e.getMessage()); - } - } + LogUtils.e(TAG, "【路径迁移】" + beanType + "压缩文件复制失败,迁移终止"); } } @@ -643,7 +665,7 @@ public class BackgroundSourceUtils { * 压缩图片并保存(默认路径) */ public void compressQualityToRecivedPicture(Bitmap bitmap) { - LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】使用默认路径压缩图片"); + LogUtils.d(TAG, "【图片压缩】使用默认路径压缩图片"); String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath(); compressQualityToRecivedPicture(bitmap, defaultCompressPath); } @@ -652,59 +674,128 @@ public class BackgroundSourceUtils { * 压缩图片并保存(指定路径) */ public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) { - LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】指定路径压缩图片,目标路径:" + targetCompressPath); + LogUtils.d(TAG, "【图片压缩】指定路径压缩图片,目标路径:" + targetCompressPath); if (bitmap == null || bitmap.isRecycled()) { ToastUtils.show("压缩失败:图片为空"); - LogUtils.e(TAG, "Bitmap为空或已回收"); + LogUtils.e(TAG, "【图片压缩】失败:Bitmap为空或已回收"); + return; + } + if (TextUtils.isEmpty(targetCompressPath)) { + ToastUtils.show("压缩失败:目标路径为空"); + LogUtils.e(TAG, "【图片压缩】失败:目标路径为空"); return; } OutputStream outStream = null; FileOutputStream fos = null; try { - LogUtils.d(TAG, "Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); + LogUtils.d(TAG, "【图片压缩】Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); File targetCompressFile = new File(targetCompressPath); if (targetCompressFile.exists()) { targetCompressFile.delete(); + LogUtils.d(TAG, "【图片压缩】已删除旧压缩文件"); } targetCompressFile.createNewFile(); fos = new FileOutputStream(targetCompressFile); outStream = new BufferedOutputStream(fos); - boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); + boolean compressSuccess = bitmap.compress(COMPRESS_FORMAT, BITMAP_COMPRESS_QUALITY, outStream); outStream.flush(); 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"); + 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()) { bitmap.recycle(); + LogUtils.d(TAG, "【图片压缩】Bitmap已回收"); } } } + + // ====================== 辅助方法(属性拷贝)====================== + /** + * 拷贝BackgroundBean属性(深拷贝) + */ + private void copyBackgroundBeanProperties(BackgroundBean source, BackgroundBean target) { + target.setBackgroundFileName(source.getBackgroundFileName()); + target.setBackgroundFilePath(source.getBackgroundFilePath()); + target.setBackgroundFileInfo(source.getBackgroundFileInfo()); + target.setIsUseBackgroundFile(source.isUseBackgroundFile()); + target.setBackgroundScaledCompressFileName(source.getBackgroundScaledCompressFileName()); + target.setBackgroundScaledCompressFilePath(source.getBackgroundScaledCompressFilePath()); + target.setIsUseBackgroundScaledCompressFile(source.isUseBackgroundScaledCompressFile()); + target.setBackgroundWidth(source.getBackgroundWidth()); + target.setBackgroundHeight(source.getBackgroundHeight()); + target.setPixelColor(source.getPixelColor()); + } + + // ====================== 对外提供的getter方法 ====================== + public BackgroundBean getCurrentBackgroundBean() { + return currentBackgroundBean; + } + + public BackgroundBean getPreviewBackgroundBean() { + return previewBackgroundBean; + } + + public String getPreviewBackgroundScaledCompressFilePath() { + String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); + if (TextUtils.isEmpty(compressFileName)) { + LogUtils.e(TAG, "【路径获取】预览压缩背景文件名为空"); + return ""; + } + File file = new File(fBackgroundCompressDir, compressFileName); + return file.getAbsolutePath(); + } + + public String getCurrentBackgroundScaledCompressFilePath() { + String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName(); + if (TextUtils.isEmpty(compressFileName)) { + LogUtils.e(TAG, "【路径获取】正式压缩背景文件名为空"); + return ""; + } + File file = new File(fBackgroundCompressDir, compressFileName); + return file.getAbsolutePath(); + } + + public String getBackgroundSourceDirPath() { + return fBackgroundSourceDir.getAbsolutePath(); + } + + public String getBackgroundCompressDirPath() { + return fBackgroundCompressDir.getAbsolutePath(); + } + + public String getCropCacheDir() { + return fCropCacheDir.getAbsolutePath(); + } + + public String getFileProviderAuthority() { + return FILE_PROVIDER_AUTHORITY; + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BatteryUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BatteryUtils.java index fb28e4b..c55c3e8 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BatteryUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BatteryUtils.java @@ -16,9 +16,10 @@ public class BatteryUtils { public static final String TAG = "BatteryUtils"; // 电池电量计算常量 - private static final int BATTERY_SCALE_DEFAULT = 100; - private static final int BATTERY_LEVEL_MIN = 0; - private static final int BATTERY_LEVEL_MAX = 100; + private static final int BATTERY_SCALE_DEFAULT = 100; // 电量刻度默认值 + private static final int BATTERY_LEVEL_MIN = 0; // 电量百分比最小值 + private static final int BATTERY_LEVEL_MAX = 100; // 电量百分比最大值 + private static final int EXTRA_STATUS_DEFAULT = -1; // 电池状态默认值 // ================================== 工具方法(静态方法,无状态设计)================================= /** @@ -27,16 +28,21 @@ public class BatteryUtils { * @return true=充电中/已充满,false=未充电 */ public static boolean isCharging(Intent intent) { - LogUtils.d(TAG, "isCharging: 调用 | intent=" + intent); + LogUtils.d(TAG, "【isCharging】调用开始"); // 入参非空校验 if (intent == null) { - LogUtils.e(TAG, "isCharging: intent为空,返回false"); + LogUtils.e(TAG, "【isCharging】入参异常:intent为空,返回false"); return false; } - int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL; - LogUtils.d(TAG, "isCharging: 解析完成 | status=" + status + " | result=" + isCharging); + // 解析电池状态 + int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, EXTRA_STATUS_DEFAULT); + LogUtils.d(TAG, "【isCharging】解析电池状态:status=" + status); + + // 判断充电状态(充电中/已充满均视为充电状态) + boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + LogUtils.d(TAG, "【isCharging】调用结束 | 充电状态=" + isCharging); return isCharging; } @@ -46,29 +52,30 @@ public class BatteryUtils { * @return 电量百分比,异常返回0 */ public static int getCurrentBatteryLevel(Intent intent) { - LogUtils.d(TAG, "getCurrentBatteryLevel: 调用 | intent=" + intent); + LogUtils.d(TAG, "【getCurrentBatteryLevel】调用开始"); // 入参非空校验 if (intent == null) { - LogUtils.e(TAG, "getCurrentBatteryLevel: intent为空,返回0"); + LogUtils.e(TAG, "【getCurrentBatteryLevel】入参异常:intent为空,返回0"); return BATTERY_LEVEL_MIN; } // 解析电量原始值与刻度值 int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_MIN); int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE_DEFAULT); - LogUtils.d(TAG, "getCurrentBatteryLevel: 原始值 | level=" + level + " | scale=" + scale); + LogUtils.d(TAG, "【getCurrentBatteryLevel】解析原始数据 | level=" + level + " | scale=" + scale); // 计算并校验电量百分比,避免除以0或数值越界 int batteryLevel; if (scale <= 0) { + LogUtils.w(TAG, "【getCurrentBatteryLevel】刻度值无效(scale=" + scale + "),直接使用level值"); batteryLevel = level; - LogUtils.w(TAG, "getCurrentBatteryLevel: scale无效,直接使用level值"); } else { batteryLevel = level * BATTERY_SCALE_DEFAULT / scale; } + // 确保电量值在0-100范围内 batteryLevel = Math.max(BATTERY_LEVEL_MIN, Math.min(batteryLevel, BATTERY_LEVEL_MAX)); - LogUtils.d(TAG, "getCurrentBatteryLevel: 计算完成 | batteryLevel=" + batteryLevel + "%"); + LogUtils.d(TAG, "【getCurrentBatteryLevel】调用结束 | 电量百分比=" + batteryLevel + "%"); return batteryLevel; } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java index 3d2b334..8f39cc6 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BitmapCacheUtils.java @@ -2,6 +2,7 @@ package cc.winboll.studio.powerbell.utils; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; @@ -23,26 +24,34 @@ import java.util.concurrent.ConcurrentHashMap; * 核心策略:无论内存如何紧张,强制保持已缓存的Bitmap,保留图片原始品质,永不自动清理 */ public class BitmapCacheUtils { + // ================================== 静态常量区(置顶归类,消除魔法值)================================= public static final String TAG = "BitmapCacheUtils"; - // 移除最大图片尺寸限制(不压缩图片) - // 若需限制尺寸而非压缩品质,可保留此常量用于校验,不参与采样率计算 // SP 相关常量 private static final String SP_NAME = "BitmapCacheSP"; private static final String SP_KEY_LAST_CACHE_PATH = "last_cache_image_path"; - // 单例实例(volatile 保证多线程可见性) + // Bitmap 解码常量 + private static final int BITMAP_SAMPLE_SIZE_ORIGINAL = 1; // 无压缩采样率 + private static final Bitmap.Config BITMAP_CONFIG_DEFAULT = Bitmap.Config.ARGB_8888; // 全彩品质配置 + + // ================================== 成员变量(按功能分类,volatile 保证多线程可见性)================================= + // 单例实例 private static volatile BitmapCacheUtils sInstance; - // 路径-Bitmap 硬引用缓存(唯一缓存,极致强制保持,任何情况不自动回收) + // 路径-Bitmap 硬引用缓存(极致强制保持,永不自动回收) private final Map mHardCacheMap; - // 路径-引用计数 映射(解决多实例共享问题,仅用于统计,不影响缓存生命周期) + // 路径-引用计数 映射(仅统计,不影响缓存生命周期) private final Map mRefCountMap; // SP 实例(用于持久化最后缓存路径) private final SharedPreferences mSp; - // 私有构造器(单例模式) + // ================================== 单例方法(双重校验锁,线程安全)================================= + /** + * 私有构造器(单例模式) + */ private BitmapCacheUtils() { - // 使用ConcurrentHashMap保证线程安全,避免手动同步 + LogUtils.d(TAG, "【BitmapCacheUtils】单例构造开始"); + // 使用 ConcurrentHashMap 保证线程安全,避免手动同步 mHardCacheMap = new ConcurrentHashMap<>(); mRefCountMap = new ConcurrentHashMap<>(); // 初始化 SP(使用 App 全局上下文,避免内存泄漏) @@ -51,6 +60,7 @@ public class BitmapCacheUtils { preloadLastCachedBitmap(); // 注册内存状态监听(仅记录日志,不清理缓存) registerMemoryStatusListener(); + LogUtils.d(TAG, "【BitmapCacheUtils】单例构造完成,极致强制缓存策略已启用"); } /** @@ -67,14 +77,14 @@ public class BitmapCacheUtils { return sInstance; } - // ====================================== 保留核心监控方法:保证与App类兼容 ====================================== + // ================================== 对外监控接口(App 类调用专用)================================= /** - * 获取当前缓存的Bitmap数量(App类调用专用) - * @return 缓存的Bitmap数量 + * 获取当前缓存的 Bitmap 数量 + * @return 缓存的 Bitmap 数量 */ public int getCacheCount() { int count = mHardCacheMap.size(); - LogUtils.d(TAG, "getCacheCount: 当前缓存Bitmap数量 - " + count); + LogUtils.d(TAG, "【getCacheCount】当前缓存 Bitmap 数量 - " + count); return count; } @@ -83,7 +93,9 @@ public class BitmapCacheUtils { * @return 路径集合 */ public Set getCachedPaths() { - return mHardCacheMap.keySet(); + Set paths = mHardCacheMap.keySet(); + LogUtils.d(TAG, "【getCachedPaths】当前缓存路径数量 - " + paths.size()); + return paths; } /** @@ -101,20 +113,22 @@ public class BitmapCacheUtils { } } } - LogUtils.d(TAG, "getTotalCacheSize: 当前缓存总内存占用 - " + totalSize + " 字节"); + LogUtils.d(TAG, "【getTotalCacheSize】当前缓存总内存占用 - " + totalSize + " 字节"); return totalSize; } - // ====================================== 核心接口:缓存操作(无压缩) ====================================== + // ================================== 对外核心接口:缓存操作(无压缩)================================= /** - * 补充接口:直接缓存已解码的Bitmap(适配BackgroundView改进需求) + * 直接缓存已解码的 Bitmap(适配 BackgroundView 改进需求) * @param imagePath 图片绝对路径 - * @param bitmap 已解码的有效Bitmap - * @return 缓存后的Bitmap / null(参数无效) + * @param bitmap 已解码的有效 Bitmap + * @return 缓存后的 Bitmap / null(参数无效) */ public Bitmap cacheBitmap(String imagePath, Bitmap bitmap) { + LogUtils.d(TAG, "【cacheBitmap】调用开始(直接缓存已解码 Bitmap)| 路径=" + imagePath); + // 入参非空校验 if (TextUtils.isEmpty(imagePath) || !isBitmapValid(bitmap)) { - LogUtils.e(TAG, "cacheBitmap: 路径或Bitmap无效"); + LogUtils.e(TAG, "【cacheBitmap】入参异常:路径为空或 Bitmap 无效"); return null; } @@ -124,35 +138,39 @@ public class BitmapCacheUtils { mRefCountMap.putIfAbsent(imagePath, 1); // 持久化当前路径到 SP saveLastCachePathToSp(imagePath); - LogUtils.d(TAG, "cacheBitmap: 直接缓存已解码Bitmap成功(极致强制保持,无压缩) - " + imagePath); + LogUtils.d(TAG, "【cacheBitmap】调用成功(直接缓存已解码 Bitmap)| 路径=" + imagePath); return bitmap; } /** - * 核心接口:根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP + * 根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP * @param imagePath 图片绝对路径 * @return 缓存成功的 Bitmap / null(路径无效/文件不存在/解码失败) */ public Bitmap cacheBitmap(String imagePath) { + LogUtils.d(TAG, "【cacheBitmap】调用开始(路径缓存)| 路径=" + imagePath); + // 入参非空校验 if (TextUtils.isEmpty(imagePath)) { - LogUtils.e(TAG, "cacheBitmap: 图片路径为空"); + LogUtils.e(TAG, "【cacheBitmap】入参异常:图片路径为空"); return null; } + // 文件有效性校验 File imageFile = new File(imagePath); if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { - LogUtils.e(TAG, "cacheBitmap: 图片文件无效(不存在/非文件/空文件) - " + imagePath); + LogUtils.e(TAG, "【cacheBitmap】文件无效:不存在/非文件/空文件 | 路径=" + imagePath); return null; } // 已缓存则直接返回,避免重复加载 Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath); if (isBitmapValid(hardCacheBitmap)) { - LogUtils.d(TAG, "cacheBitmap: 硬引用缓存命中,引用计数+1 - " + imagePath); + LogUtils.d(TAG, "【cacheBitmap】硬引用缓存命中,引用计数+1 | 路径=" + imagePath); // 引用计数+1 increaseRefCount(imagePath); // 持久化当前路径到 SP saveLastCachePathToSp(imagePath); + LogUtils.d(TAG, "【cacheBitmap】调用成功(缓存命中)| 路径=" + imagePath); return hardCacheBitmap; } @@ -165,41 +183,47 @@ public class BitmapCacheUtils { mRefCountMap.put(imagePath, 1); // 持久化当前路径到 SP saveLastCachePathToSp(imagePath); - LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径(极致强制保持,无压缩) - " + imagePath); + LogUtils.d(TAG, "【cacheBitmap】调用成功(新缓存)| 路径=" + imagePath); } else { - LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath); + LogUtils.e(TAG, "【cacheBitmap】调用失败:图片解码失败 | 路径=" + imagePath); } return bitmap; } /** - * 核心接口:根据路径获取缓存的 Bitmap + * 根据路径获取缓存的 Bitmap * @param imagePath 图片绝对路径 * @return 缓存的有效 Bitmap / null(未缓存/已回收) */ public Bitmap getCachedBitmap(String imagePath) { + LogUtils.d(TAG, "【getCachedBitmap】调用开始 | 路径=" + imagePath); + // 入参非空校验 if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "【getCachedBitmap】入参异常:图片路径为空"); return null; } // 仅从硬引用缓存获取,无任何 fallback Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath); if (isBitmapValid(hardCacheBitmap)) { + LogUtils.d(TAG, "【getCachedBitmap】调用成功(缓存命中)| 路径=" + imagePath); return hardCacheBitmap; } - // 缓存未命中或Bitmap已失效(极致强制策略下,理论上不会出现已回收情况) - LogUtils.w(TAG, "getCachedBitmap: 缓存未命中或Bitmap已失效 - " + imagePath); + // 缓存未命中或 Bitmap 已失效(极致强制策略下,理论上不会出现已回收情况) + LogUtils.w(TAG, "【getCachedBitmap】调用失败:缓存未命中或 Bitmap 已失效 | 路径=" + imagePath); return null; } - // ====================================== 引用计数管理(仅统计,不影响缓存) ====================================== + // ================================== 对外接口:引用计数管理(仅统计,不影响缓存)================================= /** - * 新增接口:增加指定路径Bitmap的引用计数 + * 增加指定路径 Bitmap 的引用计数 * @param imagePath 图片绝对路径 */ public void increaseRefCount(String imagePath) { + LogUtils.d(TAG, "【increaseRefCount】调用开始 | 路径=" + imagePath); if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "【increaseRefCount】入参异常:图片路径为空"); return; } synchronized (mRefCountMap) { @@ -209,44 +233,48 @@ public class BitmapCacheUtils { } else { mRefCountMap.put(imagePath, count + 1); } - LogUtils.d(TAG, "increaseRefCount: " + imagePath + " 引用计数变为 " + mRefCountMap.get(imagePath)); + int newCount = mRefCountMap.get(imagePath); + LogUtils.d(TAG, "【increaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数=" + newCount); } } /** - * 新增接口:减少指定路径Bitmap的引用计数,计数为0时仅标记不回收(极致强制缓存策略) + * 减少指定路径 Bitmap 的引用计数,计数为0时仅标记不回收(极致强制缓存策略) * @param imagePath 图片绝对路径 */ public void decreaseRefCount(String imagePath) { + LogUtils.d(TAG, "【decreaseRefCount】调用开始 | 路径=" + imagePath); if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "【decreaseRefCount】入参异常:图片路径为空"); return; } synchronized (mRefCountMap) { Integer count = mRefCountMap.get(imagePath); if (count == null || count <= 0) { + LogUtils.w(TAG, "【decreaseRefCount】引用计数无效:路径=" + imagePath); return; } int newCount = count - 1; if (newCount <= 0) { - // 极致强制缓存策略:引用计数为0时仅移除计数,绝对不回收Bitmap + // 极致强制缓存策略:引用计数为0时仅移除计数,绝对不回收 Bitmap mRefCountMap.remove(imagePath); - LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数为0,极致强制保持Bitmap"); + LogUtils.d(TAG, "【decreaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数为0,极致强制保持 Bitmap"); } else { mRefCountMap.put(imagePath, newCount); - LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数变为 " + newCount); + LogUtils.d(TAG, "【decreaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数=" + newCount); } } } - // ====================================== 缓存清理(仅手动调用,永不自动执行) ====================================== + // ================================== 对外接口:缓存清理(仅手动调用,永不自动执行)================================= /** * 清空所有 Bitmap 缓存(仅手动调用时执行,任何情况不自动执行) */ public void clearAllCache() { - LogUtils.w(TAG, "clearAllCache: 手动清空所有缓存(极致强制缓存策略下,需谨慎使用)"); + LogUtils.w(TAG, "【clearAllCache】调用开始(极致强制缓存策略下,需谨慎使用)"); - // 清空硬引用缓存并回收Bitmap + // 清空硬引用缓存并回收 Bitmap for (Bitmap bitmap : mHardCacheMap.values()) { if (isBitmapValid(bitmap)) { bitmap.recycle(); @@ -260,7 +288,7 @@ public class BitmapCacheUtils { // 清空 SP 中保存的最后缓存路径 clearLastCachePathInSp(); - LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空"); + LogUtils.d(TAG, "【clearAllCache】调用成功:所有 Bitmap 缓存已清空"); } /** @@ -268,16 +296,18 @@ public class BitmapCacheUtils { * @param imagePath 图片绝对路径 */ public void removeCachedBitmap(String imagePath) { + LogUtils.d(TAG, "【removeCachedBitmap】调用开始 | 路径=" + imagePath); if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "【removeCachedBitmap】入参异常:图片路径为空"); return; } synchronized (mRefCountMap) { - // 手动移除时才回收Bitmap + // 手动移除时才回收 Bitmap Bitmap hardBitmap = mHardCacheMap.remove(imagePath); if (isBitmapValid(hardBitmap)) { hardBitmap.recycle(); - LogUtils.d(TAG, "removeCachedBitmap: 手动回收硬引用缓存 - " + imagePath); + LogUtils.d(TAG, "【removeCachedBitmap】手动回收硬引用缓存 | 路径=" + imagePath); } mRefCountMap.remove(imagePath); @@ -285,22 +315,24 @@ public class BitmapCacheUtils { String lastPath = getLastCachePathFromSp(); if (imagePath.equals(lastPath)) { clearLastCachePathInSp(); - LogUtils.d(TAG, "removeCachedBitmap: 移除的是最后缓存路径,已清空 SP"); + LogUtils.d(TAG, "【removeCachedBitmap】移除最后缓存路径,已清空 SP"); } } + LogUtils.d(TAG, "【removeCachedBitmap】调用成功 | 路径=" + imagePath); } - // ====================================== 内部工具方法(无压缩解码) ====================================== + // ================================== 内部工具方法(无压缩解码 + Bitmap 有效性判断)================================= /** * 无压缩解码 Bitmap(保留原始品质) * @param imagePath 图片绝对路径 * @return 解码后的 Bitmap / null(文件无效/解码失败) */ private Bitmap decodeOriginalBitmap(String imagePath) { + LogUtils.d(TAG, "【decodeOriginalBitmap】调用开始 | 路径=" + imagePath); // 前置校验:确保文件有效 File imageFile = new File(imagePath); if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { - LogUtils.e(TAG, "decodeOriginalBitmap: 文件无效,跳过解码 - " + imagePath); + LogUtils.e(TAG, "【decodeOriginalBitmap】文件无效,跳过解码 | 路径=" + imagePath); return null; } @@ -311,47 +343,55 @@ public class BitmapCacheUtils { // 校验尺寸是否有效 if (options.outWidth <= 0 || options.outHeight <= 0) { - LogUtils.e(TAG, "decodeOriginalBitmap: 图片尺寸无效 - " + imagePath); + LogUtils.e(TAG, "【decodeOriginalBitmap】图片尺寸无效 | 路径=" + imagePath); return null; } - LogUtils.d(TAG, "decodeOriginalBitmap: 图片原始尺寸 - " + options.outWidth + "x" + options.outHeight); + LogUtils.d(TAG, "【decodeOriginalBitmap】图片原始尺寸 | 宽=" + options.outWidth + " | 高=" + options.outHeight); // 无压缩解码配置 options.inJustDecodeBounds = false; - options.inSampleSize = 1; // 不缩放,采样率为1 - options.inPreferredConfig = Bitmap.Config.ARGB_8888; // 保留全彩品质(如需节省内存可改为RGB_565,不影响品质) + options.inSampleSize = BITMAP_SAMPLE_SIZE_ORIGINAL; // 不缩放,采样率为1 + options.inPreferredConfig = BITMAP_CONFIG_DEFAULT; // 保留全彩品质 options.inPurgeable = false; // 关闭可清除标志,极致强制保持内存 options.inInputShareable = false; options.inDither = true; // 开启抖动,保证色彩还原 options.inScaled = false; // 关闭自动缩放,保留原始尺寸 try { - return BitmapFactory.decodeFile(imagePath, options); + Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options); + LogUtils.d(TAG, "【decodeOriginalBitmap】解码" + (bitmap != null ? "成功" : "失败") + " | 路径=" + imagePath); + return bitmap; } catch (OutOfMemoryError e) { - LogUtils.e(TAG, "decodeOriginalBitmap: OOM异常(无压缩,图片尺寸过大) - " + imagePath); - // 极致强制缓存策略:OOM时仅放弃当前解码,绝对不清理已缓存的Bitmap + LogUtils.e(TAG, "【decodeOriginalBitmap】OOM 异常(无压缩,图片尺寸过大)| 路径=" + imagePath); + // 极致强制缓存策略:OOM 时仅放弃当前解码,绝对不清理已缓存的 Bitmap return null; } catch (Exception e) { - LogUtils.e(TAG, "decodeOriginalBitmap: 解码异常 - " + imagePath, e); + LogUtils.e(TAG, "【decodeOriginalBitmap】解码异常 | 路径=" + imagePath, e); return null; } } /** - * 工具方法:判断Bitmap是否有效(非空且未被回收) + * 判断 Bitmap 是否有效(非空且未被回收) */ private boolean isBitmapValid(Bitmap bitmap) { - return bitmap != null && !bitmap.isRecycled(); + boolean isValid = bitmap != null && !bitmap.isRecycled(); + if (!isValid) { + LogUtils.w(TAG, "【isBitmapValid】Bitmap 无效:空或已回收"); + } + return isValid; } - // ====================================== SP 持久化相关 ====================================== + // ================================== 内部工具方法:SP 持久化相关 ================================== /** * 从 SP 中获取最后一次缓存的图片路径 * @return 最后缓存的路径 / null(未保存) */ private String getLastCachePathFromSp() { - return mSp.getString(SP_KEY_LAST_CACHE_PATH, null); + String path = mSp.getString(SP_KEY_LAST_CACHE_PATH, null); + LogUtils.d(TAG, "【getLastCachePathFromSp】获取最后缓存路径 | 路径=" + path); + return path; } /** @@ -359,11 +399,13 @@ public class BitmapCacheUtils { * @param imagePath 图片绝对路径 */ private void saveLastCachePathToSp(String imagePath) { + LogUtils.d(TAG, "【saveLastCachePathToSp】调用开始 | 路径=" + imagePath); if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "【saveLastCachePathToSp】入参异常:图片路径为空"); return; } mSp.edit().putString(SP_KEY_LAST_CACHE_PATH, imagePath).commit(); // Java 7 兼容,使用 commit 而非 apply - LogUtils.d(TAG, "saveLastCachePathToSp: 持久化最后缓存路径 - " + imagePath); + LogUtils.d(TAG, "【saveLastCachePathToSp】调用成功 | 路径=" + imagePath); } /** @@ -371,64 +413,42 @@ public class BitmapCacheUtils { */ private void clearLastCachePathInSp() { mSp.edit().remove(SP_KEY_LAST_CACHE_PATH).commit(); - LogUtils.d(TAG, "clearLastCachePathInSp: SP 中最后缓存路径已清空"); + LogUtils.d(TAG, "【clearLastCachePathInSp】调用成功:SP 中最后缓存路径已清空"); } - // ====================================== 预加载相关 ====================================== + // ================================== 内部工具方法:预加载相关 ================================== /** * 构造时预加载 SP 中保存的最后一次缓存路径的图片 */ private void preloadLastCachedBitmap() { + LogUtils.d(TAG, "【preloadLastCachedBitmap】调用开始"); String lastPath = getLastCachePathFromSp(); if (TextUtils.isEmpty(lastPath)) { - LogUtils.d(TAG, "preloadLastCachedBitmap: SP 中无保存的缓存路径,跳过预加载"); + LogUtils.d(TAG, "【preloadLastCachedBitmap】SP 中无保存的缓存路径,跳过预加载"); return; } // 调用 cacheBitmap 预加载(内部已做文件校验和缓存判断) Bitmap bitmap = cacheBitmap(lastPath); if (bitmap != null) { - LogUtils.d(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径成功(极致强制保持,无压缩) - " + lastPath); + LogUtils.d(TAG, "【preloadLastCachedBitmap】预加载成功 | 路径=" + lastPath); } else { - LogUtils.w(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径失败,清空无效路径 - " + lastPath); + LogUtils.w(TAG, "【preloadLastCachedBitmap】预加载失败,清空无效路径 | 路径=" + lastPath); // 预加载失败,清空 SP 中无效路径 clearLastCachePathInSp(); } } - // ====================================== 内存状态监听(仅记录日志) ====================================== + // ================================== 内部工具方法:内存状态监听(仅记录日志)================================= /** * 注册内存状态监听(仅记录日志,不清理缓存,极致强制缓存策略) */ private void registerMemoryStatusListener() { + LogUtils.d(TAG, "【registerMemoryStatusListener】调用开始"); if (Build.VERSION.SDK_INT >= 14) { App.getInstance().registerComponentCallbacks(new MemoryStatusCallback()); - LogUtils.d(TAG, "registerMemoryStatusListener: 内存状态监听已注册(仅记录日志,不清理缓存)"); - } - } - - /** - * 内存状态回调(仅记录日志,不清理缓存,极致强制缓存策略) - */ - private class MemoryStatusCallback implements android.content.ComponentCallbacks2 { - @Override - public void onTrimMemory(int level) { - // 极致强制缓存策略:内存紧张时仅记录日志,不清理任何缓存 - LogUtils.w(TAG, "onTrimMemory: 内存紧张级别 - " + level + ",极致强制保持所有Bitmap缓存(无压缩)"); - // 记录当前缓存状态 - logCurrentCacheStatus(); - } - - @Override - public void onLowMemory() { - // 极致强制缓存策略:低内存时仅记录日志,不清理任何缓存 - LogUtils.w(TAG, "onLowMemory: 系统低内存,极致强制保持所有Bitmap缓存(无压缩)"); - // 记录当前缓存状态 - logCurrentCacheStatus(); - } - - @Override - public void onConfigurationChanged(android.content.res.Configuration newConfig) { - // 配置变化时无需处理 + LogUtils.d(TAG, "【registerMemoryStatusListener】内存状态监听已注册(仅记录日志,不清理缓存)"); + } else { + LogUtils.w(TAG, "【registerMemoryStatusListener】API 版本低于14,不支持内存状态监听"); } } @@ -436,8 +456,35 @@ public class BitmapCacheUtils { * 记录当前缓存状态(用于内存紧张时的调试) */ private void logCurrentCacheStatus() { - LogUtils.d(TAG, "logCurrentCacheStatus: 缓存数量 - " + getCacheCount() + ",总内存占用 - " + getTotalCacheSize() + " 字节"); - LogUtils.d(TAG, "logCurrentCacheStatus: 缓存路径 - " + getCachedPaths().toString()); + LogUtils.d(TAG, "【logCurrentCacheStatus】缓存数量 - " + getCacheCount() + ",总内存占用 - " + getTotalCacheSize() + " 字节"); + LogUtils.d(TAG, "【logCurrentCacheStatus】缓存路径 - " + getCachedPaths().toString()); + } + + // ================================== 内部类:内存状态回调(仅记录日志)================================= + /** + * 内存状态回调(仅记录日志,不清理缓存,极致强制缓存策略) + */ + private class MemoryStatusCallback implements android.content.ComponentCallbacks2 { + @Override + public void onTrimMemory(int level) { + // 极致强制缓存策略:内存紧张时仅记录日志,不清理任何缓存 + LogUtils.w(TAG, "【onTrimMemory】内存紧张级别 - " + level + ",极致强制保持所有 Bitmap 缓存(无压缩)"); + // 记录当前缓存状态 + logCurrentCacheStatus(); + } + + @Override + public void onLowMemory() { + // 极致强制缓存策略:低内存时仅记录日志,不清理任何缓存 + LogUtils.w(TAG, "【onLowMemory】系统低内存,极致强制保持所有 Bitmap 缓存(无压缩)"); + // 记录当前缓存状态 + logCurrentCacheStatus(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // 配置变化时无需处理 + } } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DateUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DateUtils.java index edc55fb..3ba9cac 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DateUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DateUtils.java @@ -1,16 +1,37 @@ package cc.winboll.studio.powerbell.utils; import java.text.SimpleDateFormat; +import java.util.Locale; +import cc.winboll.studio.libappbase.LogUtils; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/24 + * @Describe 日期时间工具类(Java 7 兼容 | API 30 适配) + * 功能:提供当前时间的格式化字符串获取功能 + */ public class DateUtils { - - // 获取当前时间的格式化字符串 + // ================================== 静态常量区(置顶归类,消除魔法值)================================= + public static final String TAG = "DateUtils"; + private static final String DATE_FORMAT_PATTERN = "yyyyMMdd_HHmmssSSS"; // 修正年份格式为小写yyyy,毫秒为SSS + private static final Locale DEFAULT_LOCALE = Locale.getDefault(); + + // ================================== 工具方法(静态方法,无状态设计)================================= + /** + * 获取当前时间的格式化字符串 + * 格式:yyyyMMdd_HHmmssSSS(年-月-日_时-分-秒-毫秒) + * @return 格式化后的当前时间字符串 + */ public static String getDateNowString() { - // 日期类转化成字符串类的工具 - SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("YYYYMMdd_HHmmssmmm", java.util.Locale.getDefault()); - // 读取当前时间 - long nTimeNow = System.currentTimeMillis(); - return mSimpleDateFormat.format(nTimeNow); + LogUtils.d(TAG, "【getDateNowString】调用开始"); + // 初始化日期格式化工具(Java 7 兼容,使用小写yyyy避免周基年问题) + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_PATTERN, DEFAULT_LOCALE); + // 读取当前时间戳 + long currentTime = System.currentTimeMillis(); + // 格式化时间 + String formattedTime = sdf.format(currentTime); + LogUtils.d(TAG, "【getDateNowString】调用成功 | 格式化时间=" + formattedTime); + return formattedTime; } - } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DrawableToFileUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DrawableToFileUtils.java index d4bd3d1..aa9b695 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DrawableToFileUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/DrawableToFileUtils.java @@ -3,12 +3,10 @@ package cc.winboll.studio.powerbell.utils; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.os.Environment; -import android.util.Log; -import cc.winboll.studio.libappbase.LogUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import cc.winboll.studio.libappbase.LogUtils; /** * @Author ZhanGSKen&豆包大模型 @@ -17,85 +15,111 @@ import java.io.IOException; * 适配 PowerBell 项目:支持指定保存路径、自动创建目录、处理PNG图片压缩 */ public class DrawableToFileUtils { - private static final String TAG = "DrawableToFileUtils"; - + // ================================== 静态常量区(置顶归类,消除魔法值)================================= + public static final String TAG = "DrawableToFileUtils"; + private static final String IMAGE_FORMAT_PNG = ".png"; // 目标图片格式 + private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.PNG; // 压缩格式 + private static final int COMPRESS_QUALITY = 100; // PNG无损压缩质量 + private static final long MIN_FILE_SIZE = 100; // 有效文件最小字节数 + + // ================================== 核心工具方法(基础版:指定文件路径)================================= /** * 核心方法:将 R.drawable 图片保存为 File 对象 * @param context 上下文(用于获取 Resources) * @param drawableResId 图片资源ID(如 R.drawable.ic_test_png) - * @param fileName 保存的文件名(需带 .png 后缀,如 "test_drawable.png") + * @param filePath 保存的文件路径(可带/不带.png后缀) * @return 保存成功返回 File 对象,失败返回 null */ public static File saveDrawableToFile(Context context, int drawableResId, String filePath) { - // 1. 校验参数(避免空指针/无效参数) - if (context == null || drawableResId == 0 || filePath == null || filePath.isEmpty()) { - LogUtils.e(TAG, "【保存失败】参数无效(context为空/资源ID为0/文件名为空)"); + LogUtils.d(TAG, "【saveDrawableToFile】调用开始 | 资源ID=" + drawableResId + " | 目标路径=" + filePath); + // 1. 校验核心参数(避免空指针/无效参数) + if (context == null) { + LogUtils.e(TAG, "【saveDrawableToFile】参数异常:context为空"); return null; } - if (!filePath.endsWith(".png")) { - filePath += ".png"; // 强制添加 .png 后缀,确保图片格式正确 - LogUtils.d(TAG, "【格式适配】自动添加.png后缀,最终文件名:" + filePath); + if (drawableResId == 0) { + LogUtils.e(TAG, "【saveDrawableToFile】参数异常:drawableResId为0"); + return null; + } + if (filePath == null || filePath.isEmpty()) { + LogUtils.e(TAG, "【saveDrawableToFile】参数异常:filePath为空"); + return null; } - // 3. 构建目标 File 对象(最终保存的文件路径) - File targetFile = new File(filePath); - LogUtils.d(TAG, "【保存路径】目标文件路径:" + targetFile.getAbsolutePath()); + // 2. 格式化文件路径(强制添加.png后缀) + String targetFilePath = filePath.endsWith(IMAGE_FORMAT_PNG) ? filePath : filePath + IMAGE_FORMAT_PNG; + if (!filePath.equals(targetFilePath)) { + LogUtils.d(TAG, "【saveDrawableToFile】格式适配:自动添加.png后缀 | 最终路径=" + targetFilePath); + } - // 4. 读取 drawable 资源为 Bitmap(处理高清图/缩放问题) - Bitmap bitmap = null; - try { - // 读取 drawable 资源(适配不同分辨率的图片,避免变形) - bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId); - if (bitmap == null) { - LogUtils.e(TAG, "【读取失败】无法读取drawable资源(资源ID:" + drawableResId + ")"); + // 3. 构建目标File对象并创建父目录 + File targetFile = new File(targetFilePath); + File parentDir = targetFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + boolean isDirCreated = parentDir.mkdirs(); + if (!isDirCreated) { + LogUtils.e(TAG, "【saveDrawableToFile】目录创建失败:" + parentDir.getAbsolutePath()); return null; } - LogUtils.d(TAG, "【读取成功】drawable资源转Bitmap成功(宽:" + bitmap.getWidth() + ",高:" + bitmap.getHeight() + ")"); + LogUtils.d(TAG, "【saveDrawableToFile】目录创建成功:" + parentDir.getAbsolutePath()); + } + LogUtils.d(TAG, "【saveDrawableToFile】目标文件路径:" + targetFile.getAbsolutePath()); - // 5. 将 Bitmap 写入 File(PNG格式,无损保存) + // 4. 读取drawable资源为Bitmap + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId); + if (bitmap == null) { + LogUtils.e(TAG, "【saveDrawableToFile】读取失败:无法解析drawable资源(资源ID=" + drawableResId + ")"); + return null; + } + LogUtils.d(TAG, "【saveDrawableToFile】读取成功:Bitmap尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight()); + + // 5. 将Bitmap写入File(PNG无损保存) FileOutputStream fos = new FileOutputStream(targetFile); - // 压缩参数:PNG格式,质量100(无损),写入输出流 - boolean isSaved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); - fos.flush(); // 刷新输出流 - fos.close(); // 关闭输出流 + boolean isSaved = bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, fos); + fos.flush(); + fos.close(); - // 6. 校验保存结果(文件是否存在且有效) - if (isSaved && targetFile.exists() && targetFile.length() > 100) { - LogUtils.d(TAG, "【保存成功】drawable图片保存为File:" + targetFile.getAbsolutePath()); - return targetFile; // 保存成功,返回File对象 + // 6. 校验保存结果 + if (isSaved && targetFile.exists() && targetFile.length() > MIN_FILE_SIZE) { + LogUtils.d(TAG, "【saveDrawableToFile】保存成功:" + targetFile.getAbsolutePath()); + return targetFile; } else { - LogUtils.e(TAG, "【保存失败】图片写入文件无效(文件大小:" + (targetFile.exists() ? targetFile.length() : 0) + "字节)"); - // 保存失败,删除无效文件 + LogUtils.e(TAG, "【saveDrawableToFile】保存失败:文件无效(存在=" + targetFile.exists() + " | 大小=" + targetFile.length() + "字节)"); + // 清理无效文件 if (targetFile.exists()) { targetFile.delete(); - LogUtils.d(TAG, "【清理无效文件】已删除无效文件:" + targetFile.getAbsolutePath()); + LogUtils.d(TAG, "【saveDrawableToFile】清理无效文件:" + targetFile.getAbsolutePath()); } return null; } } catch (IOException e) { - LogUtils.e(TAG, "【保存异常】写入文件时出错:" + e.getMessage()); - LogUtils.e(TAG, "【异常堆栈】" + Log.getStackTraceString(e)); + LogUtils.e(TAG, "【saveDrawableToFile】保存异常:" + e.getMessage()); return null; } finally { // 回收Bitmap资源(避免内存溢出) if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); - LogUtils.d(TAG, "【资源回收】Bitmap资源已回收"); + LogUtils.d(TAG, "【saveDrawableToFile】资源回收:Bitmap已回收"); } } } + // ================================== 重载工具方法(扩展版:指定目录+文件名)================================= /** * 重载方法:自定义保存路径(灵活适配不同场景) * @param context 上下文 * @param drawableResId 图片资源ID * @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/") - * @param fileName 保存的文件名(带.png后缀) + * @param fileName 保存的文件名(可带/不带.png后缀) * @return 保存成功返回File对象,失败返回null */ public static File saveDrawableToFile(Context context, int drawableResId, String saveDirPath, String fileName) { - File filePath = new File(saveDirPath, fileName); - return saveDrawableToFile(context, drawableResId, filePath.getAbsolutePath()); + LogUtils.d(TAG, "【saveDrawableToFile】重载方法调用开始 | 资源ID=" + drawableResId + " | 目录=" + saveDirPath + " | 文件名=" + fileName); + // 构建完整文件路径 + File targetFile = new File(saveDirPath, fileName); + return saveDrawableToFile(context, drawableResId, targetFile.getAbsolutePath()); } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java index e76e140..ea1b0c1 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java @@ -4,33 +4,37 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import cc.winboll.studio.libappbase.LogUtils; - -import java.io.*; +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.UUID; -import android.content.Context; -import android.net.Uri; /** * 文件操作工具类 * 功能:文件读写、复制、图片转换、文件名处理等常用文件操作 - * 适配:Java 7+,支持Android全版本 + * 适配:Java 7 + Android API 30 * 注意:调用文件操作前需确保已获取存储权限(Android 6.0+ 需动态申请) */ public class FileUtils { - - /** 日志标签 */ + // ================================== 静态常量区(置顶归类,消除魔法值)================================= public static final String TAG = "FileUtils"; /** 读取文件默认缓冲区大小(10KB) */ private static final int BUFFER_SIZE = 10240; /** 最大读取文件大小(1GB),防止OOM */ private static final long MAX_READ_FILE_SIZE = 1024 * 1024 * 1024; + /** 最大文件后缀长度(避免异常文件名) */ + private static final int MAX_SUFFIX_LENGTH = 5; + /** 缓冲区大小(流复制专用) */ + private static final int STREAM_BUFFER_SIZE = 1024; - // ====================================== 文件读取相关 ====================================== - + // ================================== 文件读取相关(字符串 + 字节数组)================================= /** * 读取文件内容并转为字符串 * @param filePath 文件绝对路径(非空) @@ -38,25 +42,34 @@ public class FileUtils { * @throws IOException 异常:文件不存在、文件过大、读取失败等 */ public static String readFileAsString(String filePath) throws IOException { + LogUtils.d(TAG, "【readFileAsString】调用开始 | 文件路径=" + filePath); // 1. 校验文件合法性 File file = new File(filePath); if (!file.exists()) { + LogUtils.e(TAG, "【readFileAsString】文件不存在:" + filePath); throw new FileNotFoundException("文件不存在:" + filePath); } if (file.length() > MAX_READ_FILE_SIZE) { + LogUtils.e(TAG, "【readFileAsString】文件过大(超过1GB):" + filePath); throw new IOException("文件过大(超过1GB),禁止读取:" + filePath); } // 2. 读取文件内容(使用StringBuilder高效拼接) StringBuilder sb = new StringBuilder((int) file.length()); - try (FileInputStream fis = new FileInputStream(file)) { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); byte[] buffer = new byte[BUFFER_SIZE]; int readLen; - // 循环读取缓冲区,避免一次性读取大文件导致OOM while ((readLen = fis.read(buffer)) > 0) { sb.append(new String(buffer, 0, readLen)); } + } finally { + if (fis != null) { + fis.close(); + } } + LogUtils.d(TAG, "【readFileAsString】读取成功 | 文件大小=" + file.length() + "字节"); return sb.toString(); } @@ -67,28 +80,39 @@ public class FileUtils { * @throws IOException 异常:文件不存在、读取失败等 */ public static byte[] readFileByBytes(String filePath) throws IOException { + LogUtils.d(TAG, "【readFileByBytes】调用开始 | 文件路径=" + filePath); // 1. 校验文件合法性 File file = new File(filePath); if (!file.exists()) { + LogUtils.e(TAG, "【readFileByBytes】文件不存在:" + filePath); throw new FileNotFoundException("文件不存在:" + filePath); } // 2. 缓冲流读取(高效,减少IO次数) - try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length()); - BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { - + ByteArrayOutputStream bos = null; + BufferedInputStream bis = null; + try { + bos = new ByteArrayOutputStream((int) file.length()); + bis = new BufferedInputStream(new FileInputStream(file)); byte[] buffer = new byte[BUFFER_SIZE]; int readLen; while ((readLen = bis.read(buffer)) != -1) { bos.write(buffer, 0, readLen); } bos.flush(); + LogUtils.d(TAG, "【readFileByBytes】读取成功 | 文件大小=" + file.length() + "字节"); return bos.toByteArray(); + } finally { + if (bis != null) { + bis.close(); + } + if (bos != null) { + bos.close(); + } } } - // ====================================== 文件复制相关 ====================================== - + // ================================== 文件复制相关(FileChannel + 简化版 + 流复制)================================= /** * 基于FileChannel复制文件(高效,适用于大文件复制) * @param source 源文件(非空,必须存在) @@ -96,62 +120,117 @@ public class FileUtils { * @throws IOException 异常:源文件不存在、复制失败等 */ public static void copyFileUsingFileChannels(File source, File dest) throws IOException { + LogUtils.d(TAG, "【copyFileUsingFileChannels】调用开始 | 源文件=" + source.getAbsolutePath() + " | 目标文件=" + dest.getAbsolutePath()); // 1. 校验源文件合法性 if (!source.exists() || !source.isFile()) { + LogUtils.e(TAG, "【copyFileUsingFileChannels】源文件无效:" + source.getAbsolutePath()); throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath()); } // 2. 创建目标文件父目录 if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); + LogUtils.d(TAG, "【copyFileUsingFileChannels】创建父目录:" + dest.getParentFile().getAbsolutePath()); } - // 3. 通道复制(try-with-resources 自动关闭通道,无需手动关闭) - try (FileChannel inputChannel = new FileInputStream(source).getChannel(); - FileChannel outputChannel = new FileOutputStream(dest).getChannel()) { - // 从输入通道复制到输出通道(高效,底层优化) + // 3. 通道复制(手动关闭流,兼容Java 7) + FileChannel inputChannel = null; + FileChannel outputChannel = null; + try { + inputChannel = new FileInputStream(source).getChannel(); + outputChannel = new FileOutputStream(dest).getChannel(); outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); - LogUtils.d(TAG, "文件复制成功(FileChannel):" + source.getAbsolutePath() + " → " + dest.getAbsolutePath()); + LogUtils.d(TAG, "【copyFileUsingFileChannels】复制成功"); + } finally { + if (inputChannel != null) { + inputChannel.close(); + } + if (outputChannel != null) { + outputChannel.close(); + } } } /** - * 简化版文件复制(基于NIO Files工具类,代码简洁,适用于中小文件) + * 简化版文件复制(基于传统IO,兼容全版本,适用于中小文件) * @param oldFile 源文件(非空,必须存在) * @param newFile 目标文件(非空,父目录会自动创建) * @return 复制结果:true-成功,false-失败 */ public static boolean copyFile(File oldFile, File newFile) { + LogUtils.d(TAG, "【copyFile】调用开始 | 源文件=" + (oldFile != null ? oldFile.getAbsolutePath() : "null") + " | 目标文件=" + (newFile != null ? newFile.getAbsolutePath() : "null")); // 1. 校验源文件合法性 if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) { - LogUtils.e(TAG, "源文件无效:" + (oldFile != null ? oldFile.getAbsolutePath() : "null")); + LogUtils.e(TAG, "【copyFile】源文件无效"); return false; } // 2. 创建目标文件父目录 if (!newFile.getParentFile().exists()) { newFile.getParentFile().mkdirs(); + LogUtils.d(TAG, "【copyFile】创建父目录:" + newFile.getParentFile().getAbsolutePath()); } // 3. 复制文件(覆盖已有目标文件) + if (newFile.exists()) { + newFile.delete(); + LogUtils.d(TAG, "【copyFile】删除已有目标文件:" + newFile.getAbsolutePath()); + } + try { - Path sourcePath = Paths.get(oldFile.getPath()); - Path destPath = Paths.get(newFile.getPath()); - // 先删除已有目标文件(避免覆盖失败) - if (newFile.exists()) { - newFile.delete(); - } - Files.copy(sourcePath, destPath); - LogUtils.d(TAG, "文件复制成功(Files):" + oldFile.getAbsolutePath() + " → " + newFile.getAbsolutePath()); + copyFileUsingFileChannels(oldFile, newFile); return true; } catch (Exception e) { - LogUtils.e(TAG, "文件复制失败:" + e.getMessage(), e); + LogUtils.e(TAG, "【copyFile】复制失败:" + e.getMessage(), e); return false; } } - // ====================================== 图片文件相关 ====================================== + /** + * 复制输入流到文件(兼容Uri解析失败场景) + * @param inputStream 输入流(非空) + * @param file 目标文件(非空) + * @throws IOException 异常:流关闭失败、目录创建失败等 + */ + public static void copyStreamToFile(InputStream inputStream, File file) throws IOException { + LogUtils.d(TAG, "【copyStreamToFile】调用开始 | 目标文件=" + file.getAbsolutePath()); + // 1. 校验参数合法性 + if (inputStream == null || file == null) { + LogUtils.e(TAG, "【copyStreamToFile】参数为空:InputStream=" + (inputStream == null) + " | File=" + (file == null)); + throw new IllegalArgumentException("InputStream或File不能为空"); + } + // 2. 创建目标文件父目录 + File parentDir = file.getParentFile(); + if (!parentDir.exists() && !parentDir.mkdirs()) { + LogUtils.e(TAG, "【copyStreamToFile】无法创建父目录:" + parentDir.getAbsolutePath()); + throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath()); + } + + // 3. 流复制(手动关闭流,兼容Java 7) + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(file); + byte[] buffer = new byte[STREAM_BUFFER_SIZE]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + outputStream.flush(); + LogUtils.d(TAG, "【copyStreamToFile】复制成功"); + } finally { + try { + inputStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【copyStreamToFile】关闭输入流失败:" + e.getMessage()); + } + if (outputStream != null) { + outputStream.close(); + } + } + } + + // ================================== 图片文件相关(BitmapDrawable 获取)================================= /** * 从文件路径获取BitmapDrawable(适用于Android图片显示) * @param path 图片文件绝对路径(非空) @@ -159,40 +238,50 @@ public class FileUtils { * @throws IOException 异常:文件读取IO错误 */ public static BitmapDrawable getImageDrawable(String path) throws IOException { + LogUtils.d(TAG, "【getImageDrawable】调用开始 | 图片路径=" + path); // 1. 校验文件合法性 File file = new File(path); if (!file.exists() || !file.isFile()) { - LogUtils.e(TAG, "图片文件不存在:" + path); + LogUtils.e(TAG, "【getImageDrawable】图片文件无效:" + path); return null; } // 2. 读取文件并转为BitmapDrawable(缓冲流读取,减少内存占用) - try (InputStream is = new FileInputStream(file); - ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - + InputStream is = null; + ByteArrayOutputStream bos = null; + try { + is = new FileInputStream(file); + bos = new ByteArrayOutputStream(); byte[] buffer = new byte[BUFFER_SIZE]; int readLen; while ((readLen = is.read(buffer)) != -1) { bos.write(buffer, 0, readLen); } - - // 3. 生成Bitmap并包装为BitmapDrawable byte[] imageBytes = bos.toByteArray(); Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); + LogUtils.d(TAG, "【getImageDrawable】转换成功 | 图片尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight()); return new BitmapDrawable(bitmap); + } finally { + if (is != null) { + is.close(); + } + if (bos != null) { + bos.close(); + } } } - // ====================================== 文件名处理相关 ====================================== - + // ================================== 文件名处理相关(后缀截取 + 唯一文件名)================================= /** * 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png") * @param file 目标文件(可为null) * @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串 */ public static String getFileSuffixWithMultiDot(File file) { + LogUtils.d(TAG, "【getFileSuffixWithMultiDot】调用开始 | 文件=" + (file != null ? file.getAbsolutePath() : "null")); // 1. 校验文件合法性 if (file == null || !file.isFile()) { + LogUtils.d(TAG, "【getFileSuffixWithMultiDot】文件无效,返回空后缀"); return ""; } @@ -201,14 +290,39 @@ public class FileUtils { int lastDotIndex = fileName.lastIndexOf("."); // 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件) - if (lastDotIndex == -1 // 无 "." - || lastDotIndex == fileName.length() - 1 // 以 "." 结尾(如".gitignore") - || (fileName.length() - lastDotIndex) > 5) { // 后缀长度超过5(异常文件名) + if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1 || (fileName.length() - lastDotIndex) > MAX_SUFFIX_LENGTH) { + LogUtils.d(TAG, "【getFileSuffixWithMultiDot】无有效后缀 | 文件名=" + fileName); return ""; } // 4. 返回小写后缀(统一格式,避免大小写不一致问题) - return fileName.substring(lastDotIndex).toLowerCase(); + String suffix = fileName.substring(lastDotIndex).toLowerCase(); + LogUtils.d(TAG, "【getFileSuffixWithMultiDot】获取成功 | 后缀=" + suffix); + return suffix; + } + + /** + * 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景) + * @param file 目标文件 + * @return 后缀字符串(无后缀返回空字符串,非空统一小写) + */ + public static String getFileSuffix(File file) { + LogUtils.d(TAG, "【getFileSuffix】调用开始 | 文件=" + (file != null ? file.getAbsolutePath() : "null")); + if (file == null || file.getName().isEmpty()) { + LogUtils.d(TAG, "【getFileSuffix】文件无效,返回空后缀"); + return ""; + } + String fileName = file.getName(); + int lastDotIndex = fileName.lastIndexOf("."); + // 无后缀(没有点,或点在开头/结尾) + if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) { + LogUtils.d(TAG, "【getFileSuffix】无有效后缀 | 文件名=" + fileName); + return ""; + } + // 截取后缀并转小写(统一格式,避免 PNG/png 差异) + String suffix = fileName.substring(lastDotIndex + 1).toLowerCase(); + LogUtils.d(TAG, "【getFileSuffix】获取成功 | 后缀=" + suffix); + return suffix; } /** @@ -218,73 +332,35 @@ public class FileUtils { * @return 唯一文件名(如"a1b2c3d4e5f6_1730000000000.jpg",无后缀则不带点) */ public static String createUniqueFileName(File refFile) { + LogUtils.d(TAG, "【createUniqueFileName】调用开始 | 参考文件=" + (refFile != null ? refFile.getAbsolutePath() : "null")); // 1. 获取参考文件的后缀名(自动容错null/无效文件) String suffix = getFileSuffixWithMultiDot(refFile); - // 2. 生成唯一标识(UUID确保全局唯一,时间戳进一步降低重复概率) - String uniqueId = UUID.randomUUID().toString().replace("-", ""); // 去掉"-"简化文件名 + String uniqueId = UUID.randomUUID().toString().replace("-", ""); long timeStamp = System.currentTimeMillis(); - // 3. 拼接文件名(分场景处理,避免多余点) + String fileName; if (suffix.isEmpty()) { - // 无后缀:唯一ID + 时间戳 - return String.format("%s_%d", uniqueId, timeStamp); + fileName = String.format("%s_%d", uniqueId, timeStamp); } else { - // 有后缀:唯一ID + 时间戳 + 后缀(无多余点) - return String.format("%s_%d%s", uniqueId, timeStamp, suffix); + fileName = String.format("%s_%d%s", uniqueId, timeStamp, suffix); } + LogUtils.d(TAG, "【createUniqueFileName】生成成功 | 文件名=" + fileName); + return fileName; } - /** - * 复制输入流到文件(兼容Uri解析失败场景) - */ - public static void copyStreamToFile(InputStream inputStream, File file) throws IOException { - if (inputStream == null || file == null) { - throw new IllegalArgumentException("InputStream或File不能为空"); - } - File parentDir = file.getParentFile(); - if (!parentDir.exists() && !parentDir.mkdirs()) { - throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath()); - } - try { - OutputStream outputStream = new FileOutputStream(file); - byte[] buffer = new byte[1024]; - int length; - while ((length = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, length); - } - outputStream.flush(); - } finally { - try { - inputStream.close(); - } catch (IOException e) { - LogUtils.e("FileUtils", "关闭输入流失败:" + e.getMessage()); - } - } - } - - public static boolean isFileExists(String path) { - File file = new File(path); - return file.exists(); - } - - /** - * 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景) - * @param file 目标文件 - * @return 后缀字符串(无后缀返回空字符串,非空统一小写) - */ - public static String getFileSuffix(File file) { - if (file == null || file.getName().isEmpty()) { - return ""; // 空文件/空文件名,返回空 - } - String fileName = file.getName(); - int lastDotIndex = fileName.lastIndexOf("."); - // 无后缀(没有点,或点在开头/结尾) - if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) { - return ""; - } - // 截取后缀并转小写(统一格式,避免 PNG/png 差异) - return fileName.substring(lastDotIndex + 1).toLowerCase(); - } + // ================================== 工具辅助方法(文件存在性判断)================================= + /** + * 判断文件是否存在 + * @param path 文件绝对路径 + * @return true-存在,false-不存在 + */ + public static boolean isFileExists(String path) { + LogUtils.d(TAG, "【isFileExists】调用开始 | 文件路径=" + path); + File file = new File(path); + boolean exists = file.exists(); + LogUtils.d(TAG, "【isFileExists】判断结果 | 路径=" + path + " | 存在=" + exists); + return exists; + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java index 687689b..e14b306 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java @@ -2,27 +2,35 @@ package cc.winboll.studio.powerbell.utils; import android.app.Activity; import android.content.Intent; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; +import android.widget.Toast; import androidx.core.content.FileProvider; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.models.BackgroundBean; import com.yalantis.ucrop.UCrop; import java.io.File; +import java.util.regex.Pattern; /** - * 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File 双传参) + * 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File/BackgroundBean 多传参) + * 适配:Java 7 + Android API 30 + * 核心策略:强制 PNG 输出,保留透明通道,统一裁剪配置 */ public class ImageCropUtils { + // ================================== 静态常量区(置顶归类,消除魔法值)================================= public static final String TAG = "ImageCropUtils"; // FileProvider 授权(与 AndroidManifest 配置一致) private static final String FILE_PROVIDER_SUFFIX = ".fileprovider"; // 强制输出格式:固定为 PNG(保留透明通道) private static final String FORCE_OUTPUT_SUFFIX = "png"; - private static final android.graphics.Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = android.graphics.Bitmap.CompressFormat.PNG; + private static final Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG; + // 图片后缀正则(用于强制替换) + private static final Pattern IMAGE_SUFFIX_PATTERN = Pattern.compile("\\.(jpg|jpeg|png|bmp|gif)$", Pattern.CASE_INSENSITIVE); - // ====================== 核心裁剪方法(强制 PNG 输出,优化逻辑)====================== + // ================================== 核心裁剪方法(重载:Uri/File/BackgroundBean)================================= /** * 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG,保留透明通道 * @param activity 上下文 @@ -40,18 +48,19 @@ public class ImageCropUtils { int aspectY, boolean isFreeCrop, int requestCode) { + LogUtils.d(TAG, "【startImageCrop】调用开始(Uri 版)| 请求码=" + requestCode); // 1. 输入参数校验 if (activity == null || activity.isFinishing()) { - LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁"); + LogUtils.e(TAG, "【startImageCrop】参数异常:Activity 无效或已销毁"); return; } if (inputUri == null || outputUri == null) { - LogUtils.e(TAG, "【裁剪异常】输入/输出 Uri 为空"); + LogUtils.e(TAG, "【startImageCrop】参数异常:输入/输出 Uri 为空"); showToast(activity, "图片 Uri 无效,无法裁剪"); return; } if (!isValidUri(activity, inputUri)) { - LogUtils.e(TAG, "【裁剪异常】输入 Uri 无效:" + inputUri); + LogUtils.e(TAG, "【startImageCrop】参数异常:输入 Uri 无效 " + inputUri); showToast(activity, "原图 Uri 无效,无法裁剪"); return; } @@ -59,22 +68,23 @@ public class ImageCropUtils { // 2. 核心:强制修正输出为 PNG(忽略原图格式,统一转 PNG) File outputFile = uriToFile(activity, outputUri); if (outputFile == null) { - LogUtils.e(TAG, "【裁剪异常】输出 Uri 转 File 失败:" + outputUri); + LogUtils.e(TAG, "【startImageCrop】转换异常:输出 Uri 转 File 失败 " + outputUri); showToast(activity, "裁剪输出路径无效"); return; } outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀 outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri + LogUtils.d(TAG, "【startImageCrop】格式修正:强制输出 PNG " + outputFile.getAbsolutePath()); // 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心) UCrop uCrop = UCrop.of(inputUri, outputUri); - uCrop.withAspectRatio(aspectX, aspectY); - UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数 + uCrop.withAspectRatio(aspectX, aspectY); + UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 4. 启动裁剪 uCrop.withOptions(options); uCrop.start(activity, requestCode); - LogUtils.d(TAG, "【裁剪启动成功(Uri 版)】强制输出 PNG(透明保留),输出路径:" + outputFile.getAbsolutePath()); + LogUtils.d(TAG, "【startImageCrop】启动成功(Uri 版)| 输出路径=" + outputFile.getAbsolutePath()); } /** @@ -94,18 +104,19 @@ public class ImageCropUtils { int aspectY, boolean isFreeCrop, int requestCode) { + LogUtils.d(TAG, "【startImageCrop】调用开始(File 版)| 请求码=" + requestCode); // 1. 输入参数校验 if (activity == null || activity.isFinishing()) { - LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁"); + LogUtils.e(TAG, "【startImageCrop】参数异常:Activity 无效或已销毁"); return; } if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) { - LogUtils.e(TAG, "【裁剪异常】输入图片文件无效"); + LogUtils.e(TAG, "【startImageCrop】参数异常:输入图片文件无效 " + (inputFile != null ? inputFile.getAbsolutePath() : "null")); showToast(activity, "无有效图片可裁剪"); return; } if (outputFile == null) { - LogUtils.e(TAG, "【裁剪异常】输出文件路径为空"); + LogUtils.e(TAG, "【startImageCrop】参数异常:输出文件路径为空"); showToast(activity, "裁剪输出路径无效"); return; } @@ -114,20 +125,27 @@ public class ImageCropUtils { Uri inputUri = getFileProviderUri(activity, inputFile); outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀 Uri outputUri = getFileProviderUri(activity, outputFile); + LogUtils.d(TAG, "【startImageCrop】格式修正:强制输出 PNG " + outputFile.getAbsolutePath()); // 3. 初始化 uCrop + 强制 PNG 配置 UCrop uCrop = UCrop.of(inputUri, outputUri); - uCrop.withAspectRatio(aspectX, aspectY); - UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数 + uCrop.withAspectRatio(aspectX, aspectY); + UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 4. 启动裁剪 uCrop.withOptions(options); uCrop.start(activity, requestCode); - LogUtils.d(TAG, "【裁剪启动成功(File 版)】强制输出 PNG(透明保留),输出路径:" + outputFile.getAbsolutePath()); + LogUtils.d(TAG, "【startImageCrop】启动成功(File 版)| 输出路径=" + outputFile.getAbsolutePath()); } /** * 【BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG,保留透明通道 + * @param activity 上下文 + * @param cropBean 背景图片 Bean + * @param aspectX 固定比例 X + * @param aspectY 固定比例 Y + * @param isFreeCrop 是否自由裁剪 + * @param requestCode 裁剪请求码 */ public static void startImageCrop(Activity activity, BackgroundBean cropBean, @@ -135,166 +153,231 @@ public class ImageCropUtils { int aspectY, boolean isFreeCrop, int requestCode) { + LogUtils.d(TAG, "【startImageCrop】调用开始(BackgroundBean 版)| 请求码=" + requestCode); + if (cropBean == null) { + LogUtils.e(TAG, "【startImageCrop】参数异常:BackgroundBean 为空"); + showToast(activity, "裁剪参数无效"); + return; + } File inputFile = new File(cropBean.getBackgroundFilePath()); File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath()); startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode); + LogUtils.d(TAG, "【startImageCrop】启动成功(BackgroundBean 版)| 输入路径=" + inputFile.getAbsolutePath()); } - // ====================== 裁剪结果处理(保持兼容,优化日志)====================== + // ================================== 裁剪结果处理(优化日志,增强容错)================================= + /** + * 处理裁剪结果 + * @param requestCode 当前请求码 + * @param resultCode 结果码 + * @param data 结果数据 + * @param cropRequestCode 裁剪请求码 + * @return 裁剪成功返回输出路径,失败返回 null + */ public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) { - if (requestCode != cropRequestCode) return null; + LogUtils.d(TAG, "【handleCropResult】调用开始 | 请求码=" + requestCode + " | 裁剪请求码=" + cropRequestCode); + if (requestCode != cropRequestCode) { + LogUtils.d(TAG, "【handleCropResult】请求码不匹配,忽略结果"); + return null; + } if (resultCode == Activity.RESULT_OK && data != null) { Uri outputUri = UCrop.getOutput(data); if (outputUri != null) { String outputPath = uriToPath(outputUri); - LogUtils.d(TAG, "【裁剪成功】强制输出 PNG(透明保留),输出路径:" + outputPath); + LogUtils.d(TAG, "【handleCropResult】裁剪成功 | 输出路径=" + outputPath); return outputPath; + } else { + LogUtils.e(TAG, "【handleCropResult】裁剪失败:输出 Uri 为空"); } } else if (resultCode == UCrop.RESULT_ERROR) { Throwable error = UCrop.getError(data); - LogUtils.e(TAG, "【裁剪失败】原因:" + (error != null ? error.getMessage() : "未知错误")); + LogUtils.e(TAG, "【handleCropResult】裁剪异常:" + (error != null ? error.getMessage() : "未知错误。")); } else { - LogUtils.d(TAG, "【裁剪取消】用户手动取消"); + LogUtils.d(TAG, "【handleCropResult】裁剪取消:用户手动取消"); } return null; } - // ====================== 辅助方法(优化适配强制 PNG 逻辑)====================== - /** 校验 Uri 有效性(确保是图片类型) */ + // ================================== 私有辅助方法(参数校验 + 格式转换 + 配置初始化)================================= + /** + * 校验 Uri 有效性(确保是图片类型) + */ private static boolean isValidUri(Activity activity, Uri uri) { try { String type = activity.getContentResolver().getType(uri); - return type != null && type.startsWith("image/"); + boolean isValid = type != null && type.startsWith("image/"); + LogUtils.d(TAG, "【isValidUri】Uri 校验结果 | " + uri + " | 有效=" + isValid); + return isValid; } catch (Exception e) { - LogUtils.e(TAG, "【Uri 校验失败】原因:" + e.getMessage()); + LogUtils.e(TAG, "【isValidUri】Uri 校验失败 " + uri, e); return false; } } - /** Uri 转 File(适配 FileProvider Uri 和普通 Uri) */ + /** + * Uri 转 File(适配 FileProvider Uri 和普通 Uri) + */ private static File uriToFile(Activity activity, Uri uri) { - if (uri == null) return null; + if (uri == null) { + LogUtils.e(TAG, "【uriToFile】参数异常:Uri 为空"); + return null; + } try { if (uri.getScheme().equals("file")) { - return new File(uri.getPath()); + File file = new File(uri.getPath()); + LogUtils.d(TAG, "【uriToFile】转换成功(普通 Uri)| " + uri + " → " + file.getAbsolutePath()); + return file; } String filePath = uri.getPath(); - if (filePath == null) return null; + if (filePath == null) { + LogUtils.e(TAG, "【uriToFile】转换失败:Uri 路径为空 " + uri); + return null; + } + // 适配 FileProvider 路径 if (filePath.contains("/external_files/")) { filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/"); } else if (filePath.contains("/cache/")) { filePath = filePath.replace("/cache/", activity.getCacheDir().getAbsolutePath() + "/"); } - return new File(filePath); + File file = new File(filePath); + LogUtils.d(TAG, "【uriToFile】转换成功(FileProvider Uri)| " + uri + " → " + file.getAbsolutePath()); + return file; } catch (Exception e) { - LogUtils.e(TAG, "【Uri 转 File 失败】uri=" + uri + ",原因:" + e.getMessage()); + LogUtils.e(TAG, "【uriToFile】转换失败 " + uri, e); return null; } } - /** Uri 提取文件路径 */ + /** + * Uri 提取文件路径 + */ private static String uriToPath(Uri uri) { - if (uri == null) return null; + if (uri == null) { + LogUtils.e(TAG, "【uriToPath】参数异常:Uri 为空"); + return null; + } try { if (uri.getScheme().equals("file")) { - return uri.getPath(); + String path = uri.getPath(); + LogUtils.d(TAG, "【uriToPath】提取成功(普通 Uri)| " + uri + " → " + path); + return path; } String path = uri.getPath(); - if (path == null) return null; + if (path == null) { + LogUtils.e(TAG, "【uriToPath】提取失败:Uri 路径为空 " + uri); + return null; + } + // 适配多种 FileProvider 前缀 String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"}; for (String prefix : prefixes) { if (path.contains(prefix)) { path = path.substring(path.indexOf(prefix) + prefix.length()); String externalRoot = android.os.Environment.getExternalStorageDirectory().getAbsolutePath(); - return externalRoot + "/" + path; + path = externalRoot + "/" + path; + LogUtils.d(TAG, "【uriToPath】提取成功(FileProvider Uri)| " + uri + " → " + path); + return path; } } + LogUtils.d(TAG, "【uriToPath】提取成功(默认路径)| " + uri + " → " + path); return path; } catch (Exception e) { - LogUtils.e(TAG, "【Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage()); + LogUtils.e(TAG, "【uriToPath】提取失败 " + uri, e); return null; } } /** * 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心) - * 移除 isPng 参数,全程用 PNG 配置 */ private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) { - + LogUtils.d(TAG, "【initCropOptions】初始化裁剪配置 | 自由裁剪=" + isFreeCrop); UCrop.Options options = new UCrop.Options(); // 裁剪模式配置(自由裁剪/固定比例) - options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪 + options.setFreeStyleCropEnabled(isFreeCrop); - // 裁剪配置(优化体验) - //options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式 - //options.setCompressionQuality(100); // 图片质量 - //options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面) - //options.setToolbarTitle("图片裁剪"); // 工具栏标题 - //options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题) - //options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色 - - - // 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式) + // 核心:强制 PNG 保留透明(固定配置,无需判断原图格式) options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩 options.setCompressionQuality(100); // PNG 100% 质量,不损失透明 options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键) options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色 options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色 - // 3. 通用 UI 配置(保持原有风格) + // 通用 UI 配置(保持原有风格) options.setHideBottomControls(true); // 隐藏底部控制栏 options.setToolbarTitle("图片裁剪"); options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white)); options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); + LogUtils.d(TAG, "【initCropOptions】配置完成:强制 PNG 输出,保留透明通道"); return options; } /** - * 修正文件后缀(强制转为 .png,覆盖原有任何图片后缀) + * 修正文件后缀(强制转为指定后缀,覆盖原有任何图片后缀) */ private static File correctFileSuffix(File originFile, String targetSuffix) { String originName = originFile.getName(); - // 强制替换所有图片后缀为 targetSuffix(避免漏改) - originName = originName.replaceAll("\\.(jpg|jpeg|png|bmp|gif)$", "") + "." + targetSuffix; - return new File(originFile.getParent(), originName); + // 强制替换所有图片后缀为 targetSuffix + String newName = IMAGE_SUFFIX_PATTERN.matcher(originName).replaceAll("") + "." + targetSuffix; + File newFile = new File(originFile.getParent(), newName); + LogUtils.d(TAG, "【correctFileSuffix】后缀修正 | " + originFile.getName() + " → " + newFile.getName()); + return newFile; } - /** 生成 FileProvider Uri(适配 Android 7.0+) */ + /** + * 生成 FileProvider Uri(适配 Android 7.0+) + */ private static Uri getFileProviderUri(Activity activity, File file) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX; - return FileProvider.getUriForFile(activity, authority, file); + Uri uri = FileProvider.getUriForFile(activity, authority, file); + LogUtils.d(TAG, "【getFileProviderUri】生成成功(Android 7.0+)| " + file.getAbsolutePath() + " → " + uri); + return uri; } else { - return Uri.fromFile(file); + Uri uri = Uri.fromFile(file); + LogUtils.d(TAG, "【getFileProviderUri】生成成功(Android 7.0-)| " + file.getAbsolutePath() + " → " + uri); + return uri; } } catch (Exception e) { - LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage()); + LogUtils.e(TAG, "【getFileProviderUri】生成失败 " + file.getAbsolutePath(), e); return null; } } - /** 显示 Toast(避免崩溃) */ + /** + * 显示 Toast(避免崩溃) + */ private static void showToast(Activity activity, String msg) { if (activity != null && !activity.isFinishing()) { - android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show(); + Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); + LogUtils.d(TAG, "【showToast】显示提示:" + msg); + } else { + LogUtils.e(TAG, "【showToast】无法显示提示:Activity 无效"); } } - // ====================== 公有辅助方法(供外部调用)====================== + // ================================== 公有辅助方法(供外部调用)================================= + /** + * 公有方法:生成 FileProvider Uri + */ public static Uri getFileProviderUriPublic(Activity activity, File file) { return getFileProviderUri(activity, file); } + /** + * 公有方法:Uri 转 File + */ public static File getFileFromUriPublic(Activity activity, Uri uri) { return uriToFile(activity, uri); } + /** + * 公有方法:Uri 提取路径 + */ public static String getPathFromUriPublic(Uri uri) { return uriToPath(uri); } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageDownloader.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageDownloader.java index 2e1641d..82d27a0 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageDownloader.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageDownloader.java @@ -1,9 +1,7 @@ package cc.winboll.studio.powerbell.utils; import android.content.Context; -import android.os.Environment; import android.text.TextUtils; -import android.util.Log; import cc.winboll.studio.libappbase.LogUtils; import okhttp3.Call; import okhttp3.Callback; @@ -18,156 +16,167 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; /** + * 图片下载工具类(单例模式) + * 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件 + * 适配:Java 7 + Android API 30 + * 核心策略:OkHttp 全局复用、7天文件过期清理、UUID 唯一文件名、内置缓存目录(无需权限) * @Author ZhanGSKen&豆包大模型 * @Date 2025/11/19 20:52 - * @Describe 图片下载工具类(单例模式) - * 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件 */ public class ImageDownloader { - public static final String TAG = "ImageDownloader"; - // 单例实例 - private static ImageDownloader sInstance; - // OkHttp 客户端(全局复用,提升性能) - private OkHttpClient mOkHttpClient; - // 缓存目录:/data/data/应用包名/cache/networkdownload - private File mCacheDir; - // 过期时间:7天(单位:毫秒),可按需调整 - private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000; + // ================================== 静态常量区(置顶归类,消除魔法值)================================= + public static final String TAG = "ImageDownloader"; + // 缓存目录子文件夹名称 + private static final String CACHE_DIR_NAME = "networkdownload"; + // 过期时间:7天(单位:毫秒) + private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000; + // OkHttp 超时配置 + private static final int CONNECT_TIMEOUT = 10; + private static final int READ_WRITE_TIMEOUT = 15; + // 文件后缀最大长度 + private static final int MAX_EXTENSION_LENGTH = 5; + // 默认文件后缀 + private static final String DEFAULT_EXTENSION = ".jpg"; + // 缓冲区大小 + private static final int BUFFER_SIZE = 1024; - /** - * 私有构造(单例模式禁止外部实例化) - * @param context 上下文(用于获取缓存目录) - */ - private ImageDownloader(Context context) { - // 初始化 OkHttp 客户端(设置超时时间) - mOkHttpClient = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) + // ================================== 成员变量(单例核心 + 全局资源)================================= + // 单例实例 + private static ImageDownloader sInstance; + // OkHttp 客户端(全局复用,提升性能) + private OkHttpClient mOkHttpClient; + // 缓存目录:/data/data/应用包名/cache/networkdownload + private File mCacheDir; + + // ================================== 单例方法(线程安全 + 应用上下文)================================= + /** + * 单例获取方法(线程安全) + * @param context 上下文(建议使用 Application 上下文避免内存泄漏) + * @return 单例实例 + */ + public static synchronized ImageDownloader getInstance(Context context) { + LogUtils.d(TAG, "【getInstance】单例获取方法调用"); + if (sInstance == null) { + // 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏 + sInstance = new ImageDownloader(context.getApplicationContext()); + LogUtils.d(TAG, "【getInstance】单例实例首次创建"); + } + return sInstance; + } + + // ================================== 构造方法(私有 + 初始化逻辑)================================= + /** + * 私有构造(单例模式禁止外部实例化) + * @param context 应用上下文 + */ + private ImageDownloader(Context context) { + LogUtils.d(TAG, "【ImageDownloader】构造方法调用,开始初始化"); + // 初始化 OkHttp 客户端(设置超时时间) + initOkHttpClient(); + // 初始化缓存目录:networkdownload + initCacheDir(context); + // 初始化时清理过期文件 + clearExpiredFiles(); + LogUtils.d(TAG, "【ImageDownloader】初始化完成"); + } + + // ================================== 核心初始化方法(OkHttp + 缓存目录)================================= + /** + * 初始化 OkHttp 客户端(全局复用) + */ + private void initOkHttpClient() { + LogUtils.d(TAG, "【initOkHttpClient】开始初始化 OkHttp 客户端"); + mOkHttpClient = new OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS) .build(); + LogUtils.d(TAG, "【initOkHttpClient】OkHttp 客户端初始化完成"); + } - // 初始化缓存目录:networkdownload - initCacheDir(context); - // 初始化时清理过期文件 - clearExpiredFiles(); - } + /** + * 初始化缓存目录:若不存在则创建 + * @param context 应用上下文 + */ + private void initCacheDir(Context context) { + LogUtils.d(TAG, "【initCacheDir】开始初始化缓存目录"); + // 获取应用内置缓存目录(无需权限) + File cacheRoot = context.getCacheDir(); + mCacheDir = new File(cacheRoot, CACHE_DIR_NAME); - /** - * 单例获取方法(线程安全) - * @param context 上下文(建议使用 Application 上下文避免内存泄漏) - * @return 单例实例 - */ - public static synchronized ImageDownloader getInstance(Context context) { - if (sInstance == null) { - // 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏 - sInstance = new ImageDownloader(context.getApplicationContext()); - } - return sInstance; - } + // 若目录不存在则创建(包括父目录) + if (!mCacheDir.exists()) { + boolean isCreated = mCacheDir.mkdirs(); + if (isCreated) { + LogUtils.d(TAG, "【initCacheDir】缓存目录创建成功:" + mCacheDir.getAbsolutePath()); + } else { + LogUtils.e(TAG, "【initCacheDir】缓存目录创建失败"); + } + } else { + LogUtils.d(TAG, "【initCacheDir】缓存目录已存在:" + mCacheDir.getAbsolutePath()); + } + } - /** - * 初始化缓存目录:若不存在则创建 - */ - private void initCacheDir(Context context) { - // 获取应用内置缓存目录(无需权限) - File cacheRoot = context.getCacheDir(); - mCacheDir = new File(cacheRoot, "networkdownload"); + // ================================== 核心业务方法(下载 + 清理 + 获取最新文件)================================= + /** + * 下载网络图片到缓存目录 + * @param imageUrl 图片网络链接 + * @param callback 下载结果回调(成功/失败) + */ + public void downloadImage(final String imageUrl, final DownloadCallback callback) { + LogUtils.d(TAG, "【downloadImage】下载方法调用 | 图片链接=" + imageUrl); + // 1. 校验参数 + if (TextUtils.isEmpty(imageUrl)) { + String errorMsg = "图片链接为空"; + LogUtils.e(TAG, "【downloadImage】参数校验失败:" + errorMsg); + if (callback != null) { + callback.onFailure(errorMsg); + } + return; + } - // 若目录不存在则创建(包括父目录) - if (!mCacheDir.exists()) { - boolean isCreated = mCacheDir.mkdirs(); - if (isCreated) { - LogUtils.d("ImageDownloader", "networkdownload 缓存目录创建成功:" + mCacheDir.getAbsolutePath()); - } else { - LogUtils.e("ImageDownloader", "networkdownload 缓存目录创建失败"); - } - } else { - LogUtils.d("ImageDownloader", "networkdownload 缓存目录已存在:" + mCacheDir.getAbsolutePath()); - } - } + if (mCacheDir == null || !mCacheDir.exists()) { + String errorMsg = "缓存目录不存在"; + LogUtils.e(TAG, "【downloadImage】参数校验失败:" + errorMsg); + if (callback != null) { + callback.onFailure(errorMsg); + } + return; + } - /** - * 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件) - */ - private void clearExpiredFiles() { - if (mCacheDir == null || !mCacheDir.exists()) { - return; - } - - File[] files = mCacheDir.listFiles(); - if (files == null || files.length == 0) { - LogUtils.d("ImageDownloader", "缓存目录无文件,无需清理"); - return; - } - - long currentTime = System.currentTimeMillis(); - int deleteCount = 0; - - // 遍历所有文件,删除过期文件 - for (File file : files) { - long lastModifyTime = file.lastModified(); - if (currentTime - lastModifyTime > EXPIRE_TIME) { - if (file.delete()) { - deleteCount++; - LogUtils.d("ImageDownloader", "删除过期文件:" + file.getName()); - } else { - LogUtils.e("ImageDownloader", "删除过期文件失败:" + file.getName()); - } - } - } - - LogUtils.d("ImageDownloader", "过期文件清理完成,共删除 " + deleteCount + " 个文件"); - } - - /** - * 下载网络图片到缓存目录 - * @param imageUrl 图片网络链接 - * @param callback 下载结果回调(成功/失败) - */ - public void downloadImage(final String imageUrl, final DownloadCallback callback) { - // 校验参数 - if (TextUtils.isEmpty(imageUrl)) { - if (callback != null) { - callback.onFailure("图片链接为空"); - } - return; - } - - if (mCacheDir == null || !mCacheDir.exists()) { - if (callback != null) { - callback.onFailure("缓存目录不存在"); - } - return; - } - - // 构建 OkHttp 请求 - Request request = new Request.Builder() + // 2. 构建 OkHttp 请求 + Request request = new Request.Builder() .url(imageUrl) .build(); - // 异步下载(避免阻塞主线程) - mOkHttpClient.newCall(request).enqueue(new Callback() { + // 3. 异步下载(避免阻塞主线程) + mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { - // 下载失败,回调主线程 + String errorMsg = "下载失败:" + e.getMessage(); + LogUtils.e(TAG, "【downloadImage】OkHttp 下载失败", e); if (callback != null) { - callback.onFailure("下载失败:" + e.getMessage()); + callback.onFailure(errorMsg); } - LogUtils.e("ImageDownloader", "图片下载失败:" + e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { + // 3.1 响应状态校验 if (!response.isSuccessful()) { - // 响应失败(如 404、500) + String errorMsg = "响应失败:" + response.code(); + LogUtils.e(TAG, "【downloadImage】响应失败,状态码:" + response.code()); if (callback != null) { - callback.onFailure("响应失败:" + response.code()); + callback.onFailure(errorMsg); + } + // 关闭响应体 + if (response.body() != null) { + response.body().close(); } - LogUtils.e("ImageDownloader", "图片响应失败,状态码:" + response.code()); return; } - // 响应成功,写入文件 + // 3.2 响应成功,写入文件 InputStream inputStream = null; FileOutputStream outputStream = null; try { @@ -179,7 +188,7 @@ public class ImageDownloader { // 写入文件 outputStream = new FileOutputStream(imageFile); - byte[] buffer = new byte[1024]; + byte[] buffer = new byte[BUFFER_SIZE]; int len; while ((len = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, len); @@ -187,30 +196,32 @@ public class ImageDownloader { outputStream.flush(); // 下载成功,回调主线程并返回文件路径 + String filePath = imageFile.getAbsolutePath(); + LogUtils.d(TAG, "【downloadImage】图片下载成功:" + filePath); if (callback != null) { - callback.onSuccess(imageFile.getAbsolutePath()); + callback.onSuccess(filePath); } - LogUtils.d("ImageDownloader", "图片下载成功:" + imageFile.getAbsolutePath()); } catch (IOException e) { + String errorMsg = "文件写入失败:" + e.getMessage(); + LogUtils.e(TAG, "【downloadImage】文件写入失败", e); if (callback != null) { - callback.onFailure("文件写入失败:" + e.getMessage()); + callback.onFailure(errorMsg); } - LogUtils.e("ImageDownloader", "图片写入失败:" + e.getMessage()); } finally { // 关闭流(Java7 手动关闭,避免资源泄漏) if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtils.e(TAG, "【downloadImage】输入流关闭失败", e); } } if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtils.e(TAG, "【downloadImage】输出流关闭失败", e); } } // 关闭响应体 @@ -220,75 +231,119 @@ public class ImageDownloader { } } }); - } + } - /** - * 获取 networkdownload 目录中最后下载的文件(按修改时间排序) - * @return 最后下载的文件路径(null 表示无文件) - */ - public String getLastDownloadedFile() { - if (mCacheDir == null || !mCacheDir.exists()) { - LogUtils.e("ImageDownloader", "缓存目录不存在"); - return null; - } + /** + * 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件) + */ + private void clearExpiredFiles() { + LogUtils.d(TAG, "【clearExpiredFiles】开始清理过期文件"); + if (mCacheDir == null || !mCacheDir.exists()) { + LogUtils.d(TAG, "【clearExpiredFiles】缓存目录不存在,无需清理"); + return; + } - File[] files = mCacheDir.listFiles(); - if (files == null || files.length == 0) { - LogUtils.d("ImageDownloader", "缓存目录无文件"); - return null; - } + File[] files = mCacheDir.listFiles(); + if (files == null || files.length == 0) { + LogUtils.d(TAG, "【clearExpiredFiles】缓存目录无文件,无需清理"); + return; + } - // 按最后修改时间降序排序,取第一个即为最新文件 - File lastFile = files[0]; - for (File file : files) { - if (file.lastModified() > lastFile.lastModified()) { - lastFile = file; - } - } + long currentTime = System.currentTimeMillis(); + int deleteCount = 0; - LogUtils.d("ImageDownloader", "最后下载的文件:" + lastFile.getAbsolutePath()); - return lastFile.getAbsolutePath(); - } + // 遍历所有文件,删除过期文件 + for (File file : files) { + long lastModifyTime = file.lastModified(); + if (currentTime - lastModifyTime > EXPIRE_TIME) { + if (file.delete()) { + deleteCount++; + LogUtils.d(TAG, "【clearExpiredFiles】删除过期文件:" + file.getName()); + } else { + LogUtils.e(TAG, "【clearExpiredFiles】删除过期文件失败:" + file.getName()); + } + } + } - /** - * 工具方法:从图片链接中提取文件后缀(如 .png、.jpg) - * @param imageUrl 图片链接 - * @return 文件后缀(含点号,若无法提取则返回 .jpg) - */ - private String getFileExtension(String imageUrl) { - if (TextUtils.isEmpty(imageUrl)) { - return ".jpg"; - } + LogUtils.d(TAG, "【clearExpiredFiles】过期文件清理完成,共删除 " + deleteCount + " 个文件"); + } - int lastDotIndex = imageUrl.lastIndexOf("."); - int lastSlashIndex = imageUrl.lastIndexOf("/"); - // 确保后缀在最后一个斜杠之后,且长度合理(1-5 个字符) - if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) { - String extension = imageUrl.substring(lastDotIndex); - if (extension.length() <= 5) { - return extension.toLowerCase(); // 统一转为小写 - } - } + /** + * 获取 networkdownload 目录中最后下载的文件(按修改时间排序) + * @return 最后下载的文件路径(null 表示无文件) + */ + public String getLastDownloadedFile() { + LogUtils.d(TAG, "【getLastDownloadedFile】获取最新下载文件"); + if (mCacheDir == null || !mCacheDir.exists()) { + LogUtils.e(TAG, "【getLastDownloadedFile】缓存目录不存在"); + return null; + } - // 无法提取后缀时,默认使用 .jpg - return ".jpg"; - } + File[] files = mCacheDir.listFiles(); + if (files == null || files.length == 0) { + LogUtils.d(TAG, "【getLastDownloadedFile】缓存目录无文件"); + return null; + } - /** - * 下载结果回调接口(Java7 接口实现) - */ - public interface DownloadCallback { - /** - * 下载成功 - * @param filePath 图片保存路径 - */ - void onSuccess(String filePath); + // 按最后修改时间降序排序,取第一个即为最新文件 + File lastFile = files[0]; + for (File file : files) { + if (file.lastModified() > lastFile.lastModified()) { + lastFile = file; + } + } - /** - * 下载失败 - * @param errorMsg 失败原因 - */ - void onFailure(String errorMsg); - } + String filePath = lastFile.getAbsolutePath(); + LogUtils.d(TAG, "【getLastDownloadedFile】最后下载的文件:" + filePath); + return filePath; + } + + // ================================== 辅助工具方法(文件后缀提取)================================= + /** + * 工具方法:从图片链接中提取文件后缀(如 .png、.jpg) + * @param imageUrl 图片链接 + * @return 文件后缀(含点号,若无法提取则返回 .jpg) + */ + private String getFileExtension(String imageUrl) { + LogUtils.d(TAG, "【getFileExtension】提取文件后缀 | 图片链接=" + imageUrl); + if (TextUtils.isEmpty(imageUrl)) { + LogUtils.d(TAG, "【getFileExtension】图片链接为空,返回默认后缀:" + DEFAULT_EXTENSION); + return DEFAULT_EXTENSION; + } + + int lastDotIndex = imageUrl.lastIndexOf("."); + int lastSlashIndex = imageUrl.lastIndexOf("/"); + // 确保后缀在最后一个斜杠之后,且长度合理(1-5 个字符) + if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) { + String extension = imageUrl.substring(lastDotIndex); + if (extension.length() <= MAX_EXTENSION_LENGTH) { + extension = extension.toLowerCase(); // 统一转为小写 + LogUtils.d(TAG, "【getFileExtension】提取后缀成功:" + extension); + return extension; + } + } + + // 无法提取后缀时,默认使用 .jpg + LogUtils.d(TAG, "【getFileExtension】无法提取有效后缀,返回默认后缀:" + DEFAULT_EXTENSION); + return DEFAULT_EXTENSION; + } + + // ================================== 下载结果回调接口(Java7 接口实现)================================= + /** + * 下载结果回调接口 + */ + public interface DownloadCallback { + /** + * 下载成功 + * @param filePath 图片保存路径 + */ + void onSuccess(String filePath); + + /** + * 下载失败 + * @param errorMsg 失败原因 + */ + void onFailure(String errorMsg); + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageUtils.java index 0528b07..229653d 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageUtils.java @@ -10,44 +10,100 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +/** + * 图片处理工具类(质量压缩专用) + * 功能:对图片进行JPEG质量压缩,并将压缩结果覆盖源文件 + * 适配:Java 7 + Android API 30 + * 核心逻辑:Bitmap.compress 质量压缩 + FileChannel 高效文件复制 + */ public class ImageUtils { - + // ================================== 静态常量区(置顶归类,消除魔法值)================================= public static final String TAG = ImageUtils.class.getSimpleName(); + private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG; + private static final int MIN_COMPRESS_QUALITY = 0; + private static final int MAX_COMPRESS_QUALITY = 100; - // 这里我们生成了一个Pic文件夹,在下面放了我们质量压缩后的图片,用于和原图对比 - // 压缩图片使用Bitmap.compress(),这里是质量压缩 - // 参数:Context context :调用本函数函数引用的资源体系 - // String szSrcImagePath :要压缩的源文件路径 - // String szDstImagePath :压缩后文件要保存的路径 - // int nPictureCompress :图片压缩比例 - public static void bitmapCompress(Context context, String szSrcImagePath, String szDstImagePath, int nPictureCompress) { + // ================================== 核心工具方法(图片质量压缩)================================= + /** + * 图片质量压缩(JPEG格式),压缩后覆盖源文件 + * @param context 上下文(备用,当前逻辑未直接使用) + * @param srcImagePath 源图片文件路径(非空,文件需存在) + * @param dstImagePath 压缩后临时保存路径(非空,用于存储压缩中间文件) + * @param compressQuality 压缩质量(0-100,数值越小压缩率越高) + */ + public static void bitmapCompress(Context context, String srcImagePath, String dstImagePath, int compressQuality) { + LogUtils.d(TAG, "【bitmapCompress】调用开始 | 源路径=" + srcImagePath + " | 临时路径=" + dstImagePath + " | 压缩质量=" + compressQuality); + // 1. 前置参数校验 + if (srcImagePath == null || srcImagePath.isEmpty()) { + LogUtils.e(TAG, "【bitmapCompress】参数异常:源文件路径为空"); + return; + } + if (dstImagePath == null || dstImagePath.isEmpty()) { + LogUtils.e(TAG, "【bitmapCompress】参数异常:临时文件路径为空"); + return; + } + if (compressQuality < MIN_COMPRESS_QUALITY || compressQuality > MAX_COMPRESS_QUALITY) { + LogUtils.e(TAG, "【bitmapCompress】参数异常:压缩质量超出范围(0-100),当前值=" + compressQuality); + return; + } + + File srcFile = new File(srcImagePath); + if (!srcFile.exists() || !srcFile.isFile()) { + LogUtils.e(TAG, "【bitmapCompress】源文件无效:不存在或不是文件 " + srcImagePath); + return; + } + + Bitmap compressBitmap = null; + OutputStream outputStream = null; try { - Bitmap bmpCompressImage; + // 2. 读取源图片为Bitmap + compressBitmap = BitmapFactory.decodeFile(srcImagePath); + if (compressBitmap == null) { + LogUtils.e(TAG, "【bitmapCompress】Bitmap解码失败:无法读取源图片 " + srcImagePath); + return; + } + LogUtils.d(TAG, "【bitmapCompress】Bitmap解码成功 | 尺寸=" + compressBitmap.getWidth() + "x" + compressBitmap.getHeight()); - //生成新的文件 - File fDstCompressImage = new File(szDstImagePath); + // 3. 创建临时压缩文件 + File dstFile = new File(dstImagePath); + File dstParentDir = dstFile.getParentFile(); + if (dstParentDir != null && !dstParentDir.exists()) { + boolean isDirCreated = dstParentDir.mkdirs(); + LogUtils.d(TAG, "【bitmapCompress】临时目录创建" + (isDirCreated ? "成功" : "失败") + ":" + dstParentDir.getAbsolutePath()); + } - //裁剪后的图像转成BitMap - //photoBitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uriClipUri)); - bmpCompressImage = BitmapFactory.decodeFile(szSrcImagePath); + // 4. 写入压缩数据 + outputStream = new FileOutputStream(dstFile); + boolean isCompressSuccess = compressBitmap.compress(COMPRESS_FORMAT, compressQuality, outputStream); + if (!isCompressSuccess) { + LogUtils.e(TAG, "【bitmapCompress】压缩失败:Bitmap.compress 执行失败"); + return; + } + LogUtils.d(TAG, "【bitmapCompress】压缩成功:临时文件已生成 " + dstFile.getAbsolutePath()); - //创建输出流 - OutputStream out = null; - - out = new FileOutputStream(fDstCompressImage.getPath()); - - //压缩文件,返回结果,参数分别是压缩的格式,压缩质量的百分比,输出流 - boolean bCompress = bmpCompressImage.compress(Bitmap.CompressFormat.JPEG, nPictureCompress, out); - - // 复制压缩后的文件到源路径 - File fSrcImage = new File(szSrcImagePath); - FileUtils.copyFileUsingFileChannels(fDstCompressImage, fSrcImage); - LogUtils.d(TAG, Integer.toString(nPictureCompress) + "%压缩结束。"); + // 5. 复制压缩文件覆盖源文件 + FileUtils.copyFileUsingFileChannels(dstFile, srcFile); + LogUtils.d(TAG, "【bitmapCompress】" + compressQuality + "%压缩结束:已覆盖源文件 " + srcImagePath); } catch (FileNotFoundException e) { - LogUtils.d(TAG, "bitmapCompress FileNotFoundException : " + e.getMessage()); + LogUtils.e(TAG, "【bitmapCompress】文件未找到异常", e); } catch (IOException e) { - LogUtils.d(TAG, "bitmapCompress IOException : " + e.getMessage()); + LogUtils.e(TAG, "【bitmapCompress】IO异常", e); + } finally { + // 6. 资源释放:关闭输出流 + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【bitmapCompress】输出流关闭失败", e); + } + } + // 7. 资源释放:回收Bitmap + if (compressBitmap != null && !compressBitmap.isRecycled()) { + compressBitmap.recycle(); + LogUtils.d(TAG, "【bitmapCompress】Bitmap资源已回收"); + } } } } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/MimoUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/MimoUtils.java deleted file mode 100644 index 28038ca..0000000 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/MimoUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package cc.winboll.studio.powerbell.utils; - -import android.content.Context; -import android.util.DisplayMetrics; - -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/14 11:14 - * @Describe 米盟 MimoUtils - */ -public final class MimoUtils { - public static final String TAG = "Utils"; - - public static int dpToPx(Context context, float dp) { - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - return (int) (dp * displayMetrics.density + 0.5f); - } - - public static int pxToDp(Context context, float px) { - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - return (int) (px / displayMetrics.density + 0.5f); - } - - public static int pxToSp(Context context, float pxValue) { - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - return (int) (pxValue / displayMetrics.scaledDensity + 0.5f); - } - - public static int spToPx(Context context, float spValue) { - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - return (int) (spValue * displayMetrics.scaledDensity + 0.5f); - } -} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ServiceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ServiceUtils.java index 58bc4ef..55133a3 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ServiceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ServiceUtils.java @@ -2,29 +2,69 @@ package cc.winboll.studio.powerbell.utils; import android.app.ActivityManager; import android.content.Context; +import android.text.TextUtils; +import cc.winboll.studio.libappbase.LogUtils; import java.util.List; +/** + * 服务状态工具类 + * 功能:判断指定服务是否处于运行状态 + * 适配:Java 7 + Android API 30 + * 注意:Android 8.0+ 对后台服务限制严格,此方法仅适用于前台服务或兼容场景 + */ public class ServiceUtils { - + // ================================== 静态常量区(置顶归类)================================= public static final String TAG = ServiceUtils.class.getSimpleName(); + // 最大查询服务数量 + private static final int MAX_RUNNING_SERVICES = 1000; - public static boolean isServiceAlive(Context context, String szServiceName) { - // 获取Activity管理者对象 - ActivityManager manager = (ActivityManager) context - .getSystemService(Context.ACTIVITY_SERVICE); - // 获取正在运行的服务(此处设置最多取1000个) - List runningServices = manager - .getRunningServices(1000); - if (runningServices.size() <= 0) { + // ================================== 核心工具方法(判断服务是否运行)================================= + /** + * 判断指定服务是否处于运行状态 + * @param context 上下文(建议使用 Application 上下文避免内存泄漏) + * @param serviceName 服务完整类名(如:com.example.app.service.DemoService) + * @return true-服务运行中,false-服务未运行或查询失败 + */ + public static boolean isServiceAlive(Context context, String serviceName) { + LogUtils.d(TAG, "【isServiceAlive】调用开始 | 服务名称=" + serviceName); + // 1. 前置参数校验 + if (context == null) { + LogUtils.e(TAG, "【isServiceAlive】参数异常:Context 为空"); return false; } - // 遍历,若存在名字和传入的serviceName的一致则说明存在 - for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) { - if (runningServiceInfo.service.getClassName().equals(szServiceName)) { + if (TextUtils.isEmpty(serviceName)) { + LogUtils.e(TAG, "【isServiceAlive】参数异常:服务名称为空"); + return false; + } + + // 2. 获取 ActivityManager + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) { + LogUtils.e(TAG, "【isServiceAlive】获取 ActivityManager 失败"); + return false; + } + + // 3. 查询正在运行的服务 + List runningServices = activityManager.getRunningServices(MAX_RUNNING_SERVICES); + if (runningServices == null || runningServices.size() <= 0) { + LogUtils.d(TAG, "【isServiceAlive】正在运行的服务列表为空"); + return false; + } + + // 4. 遍历服务列表,匹配目标服务 + for (ActivityManager.RunningServiceInfo serviceInfo : runningServices) { + if (serviceInfo.service == null) { + continue; + } + String className = serviceInfo.service.getClassName(); + if (serviceName.equals(className)) { + LogUtils.d(TAG, "【isServiceAlive】服务运行中 | 匹配成功:" + serviceName); return true; } } + LogUtils.d(TAG, "【isServiceAlive】服务未运行 | 未匹配到:" + serviceName); return false; } } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/StringUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/StringUtils.java index 88d8bbf..9cb966a 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/StringUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/StringUtils.java @@ -1,114 +1,151 @@ package cc.winboll.studio.powerbell.utils; +import android.text.TextUtils; +import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.powerbell.models.BatteryInfoBean; import java.util.ArrayList; +import java.util.Locale; +/** + * 字符串格式化工具类 + * 功能:电量使用时间列表格式化、时间跨度计算 + * 适配:Java 7 + Android API 30 + * 核心逻辑:将电池信息列表转换为指定格式字符串,计算时间戳之间的跨度并格式化 + */ public class StringUtils { + // ================================== 静态常量区(置顶归类,消除魔法值)================================= public static final String TAG = StringUtils.class.getSimpleName(); + // 时间跨度单位符号 + private static final String UNIT_DAY = "☀"; + private static final String UNIT_HOUR = "★"; + private static final String UNIT_MINUTE = "✰"; + private static final String UNIT_SECOND_DEFAULT = "☆}"; + // 时间计算常量 + private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000L; + private static final long MILLIS_PER_HOUR = 60 * 60 * 1000L; + private static final long MILLIS_PER_MINUTE = 60 * 1000L; + private static final long MILLIS_PER_SECOND = 1000L; + // 空字符串常量(替代 TextUtils.EMPTY,保证 Java 7 兼容) + private static final String EMPTY_STRING = ""; - // 电量改变使用分钟数列表 - // List of power-changing usage minutes - // - public static String formatPCMListString(ArrayList arrayListBatteryInfo) { - /* 调试数据 - Time t1 = new Time(); - //t.set(int second, int minute, int hour, int monthDay, int month, int year) {} - t1.set(4, 8, 0, 27, 4, 2022); - long ntime1 = t1.toMillis(true); - Time t2 = new Time(); - //t.set(int second, int minute, int hour, int monthDay, int month, int year) {} - t2.set(9, 12, 3, 29, 4, 2022); - long ntime2 = t2.toMillis(true); - LogUtils.d(TAG, "ntime1 is " + Long.toString(ntime1)); - LogUtils.d(TAG, "ntime2 is " + Long.toString(ntime2)); - LogUtils.d(TAG, "getTimespanDifference(ntime1, ntime2) is " + getTimespanDifference(ntime1, ntime2)); - */ - - /*String sz = ""; - for (int i = 0; i < lnTime.size() - 1; i++) { - sz += getTimespanDifference(lnTime.get(i), lnTime.get(i + 1)); - } - return sz;*/ - - String sz = ""; - for (int i = 0; i < arrayListBatteryInfo.size() - 1; i++) { - //LogUtils.d(TAG, "arrayListBatteryInfo.get(i).getBattetyValue() is "+ Integer.toString(arrayListBatteryInfo.get(i).getBattetyValue())); - sz = arrayListBatteryInfo.get(i).getBattetyValue() + "% " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz; + // ================================== 核心格式化方法(电量列表格式化)================================= + /** + * 格式化电量使用时间列表为单行字符串 + * @param batteryInfoList 电池信息列表(非空) + * @return 格式化后的单行字符串,格式:"电量% 时间跨度 电量% 时间跨度 ..." + */ + public static String formatPCMListString(ArrayList batteryInfoList) { + LogUtils.d(TAG, "【formatPCMListString】调用开始 | 列表大小=" + (batteryInfoList != null ? batteryInfoList.size() : null)); + // 1. 参数校验 + if (batteryInfoList == null || batteryInfoList.size() < 2) { + LogUtils.e(TAG, "【formatPCMListString】参数异常:列表为空或长度不足2"); + return EMPTY_STRING; } - return sz; - } - - public static String formatPCMListStringWithEnter(ArrayList arrayListBatteryInfo) { - String sz = ""; - for (int i = 0; i < arrayListBatteryInfo.size() - 1; i++) { - //LogUtils.d(TAG, "arrayListBatteryInfo.get(i).getBattetyValue() is "+ Integer.toString(arrayListBatteryInfo.get(i).getBattetyValue())); - sz = "\n" + arrayListBatteryInfo.get(i).getBattetyValue() + "%\n " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz; + + String result = EMPTY_STRING; + // 2. 遍历列表,拼接字符串(倒序拼接) + for (int i = 0; i < batteryInfoList.size() - 1; i++) { + BatteryInfoBean currentBean = batteryInfoList.get(i); + BatteryInfoBean nextBean = batteryInfoList.get(i + 1); + // 空指针防护 + if (currentBean == null || nextBean == null) { + LogUtils.w(TAG, "【formatPCMListString】列表项为空,跳过当前索引:" + i); + continue; + } + // 获取电量和时间跨度 + int batteryValue = currentBean.getBattetyValue(); + String timeSpan = getTimespanDifference(currentBean.getTimeStamp(), nextBean.getTimeStamp()); + // 倒序拼接 + result = batteryValue + "% " + timeSpan + " " + result; + LogUtils.d(TAG, "【formatPCMListString】循环拼接 | 索引=" + i + " | 电量=" + batteryValue + "% | 时间跨度=" + timeSpan); } - return sz; + + LogUtils.d(TAG, "【formatPCMListString】格式化完成 | 结果长度=" + result.length()); + return result; } - // 获取时间之间的时间跨度字符串。 - // Get timespan string between times. - // 返回值: {(几天/)(几小时/)(几分钟/)(几秒钟)} - // 返回值: {(几小时/)(几分钟/)(几秒钟)} - // 返回值: {(几分钟/)(几秒钟)} - // 返回值: {(几秒钟)} - // (注:start == end 时) 返回值: {0} + /** + * 格式化电量使用时间列表为带换行的字符串 + * @param batteryInfoList 电池信息列表(非空) + * @return 格式化后的带换行字符串,每行一个电量和时间跨度 + */ + public static String formatPCMListStringWithEnter(ArrayList batteryInfoList) { + LogUtils.d(TAG, "【formatPCMListStringWithEnter】调用开始 | 列表大小=" + (batteryInfoList != null ? batteryInfoList.size() : null)); + // 1. 参数校验 + if (batteryInfoList == null || batteryInfoList.size() < 2) { + LogUtils.e(TAG, "【formatPCMListStringWithEnter】参数异常:列表为空或长度不足2"); + return EMPTY_STRING; + } + + String result = EMPTY_STRING; + // 2. 遍历列表,拼接字符串(倒序拼接,带换行) + for (int i = 0; i < batteryInfoList.size() - 1; i++) { + BatteryInfoBean currentBean = batteryInfoList.get(i); + BatteryInfoBean nextBean = batteryInfoList.get(i + 1); + // 空指针防护 + if (currentBean == null || nextBean == null) { + LogUtils.w(TAG, "【formatPCMListStringWithEnter】列表项为空,跳过当前索引:" + i); + continue; + } + // 获取电量和时间跨度 + int batteryValue = currentBean.getBattetyValue(); + String timeSpan = getTimespanDifference(currentBean.getTimeStamp(), nextBean.getTimeStamp()); + // 倒序拼接(带换行) + result = "\n" + batteryValue + "%\n " + timeSpan + " " + result; + LogUtils.d(TAG, "【formatPCMListStringWithEnter】循环拼接 | 索引=" + i + " | 电量=" + batteryValue + "% | 时间跨度=" + timeSpan); + } + + LogUtils.d(TAG, "【formatPCMListStringWithEnter】格式化完成 | 结果长度=" + result.length()); + return result; + } + + // ================================== 时间跨度计算方法(核心工具方法)================================= + /** + * 计算两个时间戳之间的跨度并格式化为指定字符串 + * @param start 开始时间戳(毫秒) + * @param end 结束时间戳(毫秒) + * @return 格式化的时间跨度字符串,格式:{天☀时★分✰秒} 或 {☆}(当时间差为0时) + */ public static String getTimespanDifference(long start, long end) { - String szReturn = "{"; + LogUtils.d(TAG, "【getTimespanDifference】调用开始 | 开始时间戳=" + start + " | 结束时间戳=" + end); long between = end - start; - //LogUtils.d(TAG, "between is " + Long.toString(between)); - long day = between / (24 * 60 * 60 * 1000); - long hour = (between / (60 * 60 * 1000) - day * 24); - long min = ((between / (60 * 1000)) - day * 24 * 60 - hour * 60); - long s = (between / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60); - /* 调试数据 - day = 0; - hour = 2; - min = 0; - s = 7; - */ + LogUtils.d(TAG, "【getTimespanDifference】时间差(毫秒)=" + between); - //long ms = (between - day * 24 * 60 * 60 * 1000 - hour * 60 * 60 * 1000 - //- min * 60 * 1000 - s * 1000); + // 计算天、时、分、秒 + long day = between / MILLIS_PER_DAY; + long hour = (between % MILLIS_PER_DAY) / MILLIS_PER_HOUR; + long min = (between % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE; + long sec = (between % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND; - szReturn += day > 0 ? String.format(java.util.Locale.getDefault(), "%d☀", day) : ""; - szReturn += hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d★", hour) : ""; - szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d✰", min) : ""; - szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d}", s) : "☆}"; + // 拼接结果字符串 + StringBuilder result = new StringBuilder("{"); + boolean hasHigherUnit = false; - //String strmin = String.format("%02d", min); - //String strs = String.format("%02d", s); - //String strms = String.format("%03d",ms); - //String timeDifference = day + "天" + hour + "小时" + strmin + "分" + strs + "秒" + strms + "毫秒"; - //String timeDifference = hour + getString(R.string.activity_main_msg_hour) - // + strmin + getString(R.string.activity_main_msg_minute) - // + strs + getString(R.string.activity_main_msg_second); - //return timeDifference; + // 拼接天 + if (day > 0) { + result.append(String.format(Locale.getDefault(), "%d%s", day, UNIT_DAY)); + hasHigherUnit = true; + } + // 拼接时(当天>0或后续有单位时) + if (hour > 0 || hasHigherUnit) { + result.append(String.format(Locale.getDefault(), "%d%s", hour, UNIT_HOUR)); + hasHigherUnit = true; + } + // 拼接分(当时>0或后续有单位时) + if (min > 0 || hasHigherUnit) { + result.append(String.format(Locale.getDefault(), "%d%s", min, UNIT_MINUTE)); + hasHigherUnit = true; + } + // 拼接秒或默认值 + if (hasHigherUnit) { + result.append(String.format(Locale.getDefault(), "%d}", sec)); + } else { + result.append(UNIT_SECOND_DEFAULT); + } - return szReturn; + String timeSpan = result.toString(); + LogUtils.d(TAG, "【getTimespanDifference】计算完成 | 时间跨度=" + timeSpan); + return timeSpan; } - - // 调试函数: 调试formatPCMListString(ArrayList lnTime) - // - /*public static String formatPCMListString_test() { - // 调试数据 - ArrayList listTime = new ArrayList(); - Time t1 = new Time(); - //t.set(int second, int minute, int hour, int monthDay, int month, int year) {} - t1.set(0, 8, 0, 27, 4, 2022); - long ntime1 = t1.toMillis(true); - listTime.add(ntime1); - for (int i = 0; i < 5; i++) { - Time t2 = new Time(); - //t.set(int second, int minute, int hour, int monthDay, int month, int year) {} - t2.set(4, 8 + i + 1, 0, 27, 4, 2022); - long ntime2 = t2.toMillis(true); - listTime.add(ntime2); - } - - return formatPCMListString(listTime); - //LogUtils.d(TAG, StringUtils.formatPCMListString(listTime)); - - }*/ } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java index 2678004..02d107e 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java @@ -10,7 +10,6 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; -import android.widget.ImageView.ScaleType; import android.widget.LinearLayout; import android.widget.RelativeLayout; import cc.winboll.studio.libappbase.LogUtils; @@ -25,67 +24,68 @@ import java.io.File; * 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存,保留图片原始品质 */ public class BackgroundView extends RelativeLayout { - + // ====================================== 静态常量区 ====================================== public static final String TAG = "BackgroundView"; - // 记录当前已缓存的图片路径 - private String mCurrentCachedPath = ""; + // Bitmap 配置常量(原始品质) + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + private static final int BITMAP_SAMPLE_SIZE = 1; // 不缩放采样率 + // ====================================== 成员变量区 ====================================== + // 缓存相关 + private String mCurrentCachedPath = ""; + // 视图相关 private Context mContext; private LinearLayout mLlContainer; // 主容器LinearLayout private ImageView mIvBackground; // 图片显示控件 + // 图片属性 private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高) // ====================================== 构造器(Java7兼容) ====================================== public BackgroundView(Context context) { super(context); - LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ==="); + LogUtils.d(TAG, "=== BackgroundView 构造器1启动 [context] ==="); this.mContext = context; initView(); } public BackgroundView(Context context, AttributeSet attrs) { super(context, attrs); - LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ==="); + LogUtils.d(TAG, "=== BackgroundView 构造器2启动 [context, attrs] ==="); this.mContext = context; initView(); } public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ==="); + LogUtils.d(TAG, "=== BackgroundView 构造器3启动 [context, attrs, defStyleAttr] ==="); this.mContext = context; initView(); } - // ====================================== 初始化 ====================================== + // ====================================== 初始化方法 ====================================== private void initView() { LogUtils.d(TAG, "=== initView 启动 ==="); // 1. 配置当前控件:全屏+透明 setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - // 2. 初始化主容器LinearLayout initLinearLayout(); - // 3. 初始化ImageView initImageView(); - - // 初始设置透明背景 + // 4. 初始设置透明背景 setDefaultTransparentBackground(); - LogUtils.d(TAG, "=== initView 完成 ==="); } private void initLinearLayout() { LogUtils.d(TAG, "=== initLinearLayout 启动 ==="); mLlContainer = new LinearLayout(mContext); - // 配置LinearLayout:全屏+垂直方向+居中 LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT ); mLlContainer.setLayoutParams(llParams); mLlContainer.setOrientation(LinearLayout.VERTICAL); - mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中 + mLlContainer.setGravity(android.view.Gravity.CENTER); mLlContainer.setBackgroundColor(0x00000000); this.addView(mLlContainer); LogUtils.d(TAG, "=== initLinearLayout 完成 ==="); @@ -94,106 +94,118 @@ public class BackgroundView extends RelativeLayout { private void initImageView() { LogUtils.d(TAG, "=== initImageView 启动 ==="); mIvBackground = new ImageView(mContext); - // 配置ImageView:wrap_content+居中+透明背景 LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT ); mIvBackground.setLayoutParams(ivParams); - mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺(无拉伸裁剪) + mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER); mIvBackground.setBackgroundColor(0x00000000); mLlContainer.addView(mIvBackground); LogUtils.d(TAG, "=== initImageView 完成 ==="); } + // ====================================== 对外方法 ====================================== public void loadByBackgroundBean(BackgroundBean bean) { loadByBackgroundBean(bean, false); } public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) { - if (!bean.isUseBackgroundFile()) { + LogUtils.d(TAG, "=== loadByBackgroundBean 启动 [isRefresh:" + isRefresh + "] ==="); + // 参数校验 + if (bean == null) { + LogUtils.e(TAG, "loadByBackgroundBean: BackgroundBean为空"); setDefaultTransparentBackground(); return; } - String targetPath = bean.isUseBackgroundScaledCompressFile() - ? bean.getBackgroundScaledCompressFilePath() - : bean.getBackgroundFilePath(); - - if (!(new File(targetPath).exists())) { - LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath)); + // 判断是否使用背景文件 + if (!bean.isUseBackgroundFile()) { + LogUtils.d(TAG, "loadByBackgroundBean: 不使用背景文件,设置透明背景"); + setDefaultTransparentBackground(); return; } - - // 核心修改:刷新时不删除旧缓存,仅重新解码原始品质图片并更新缓存(强制保持策略) + // 获取目标路径 + String targetPath = bean.isUseBackgroundScaledCompressFile() + ? bean.getBackgroundScaledCompressFilePath() + : bean.getBackgroundFilePath(); + LogUtils.d(TAG, "loadByBackgroundBean: 目标路径=" + targetPath); + // 校验文件是否存在 + File targetFile = new File(targetPath); + if (!targetFile.exists() || !targetFile.isFile()) { + LogUtils.e(TAG, "loadByBackgroundBean: 视图控件图片不存在:" + targetPath); + return; + } + // 刷新逻辑:重新解码原始品质图片并更新缓存 if (isRefresh) { - LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存 - " + targetPath); - // 刷新时直接解码原始品质图片,更新缓存(不删除旧缓存) - Bitmap newBitmap = decodeOriginalBitmap(new File(targetPath)); + LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存"); + Bitmap newBitmap = decodeOriginalBitmap(targetFile); if (newBitmap != null) { App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap); - // 增加引用计数 App.sBitmapCacheUtils.increaseRefCount(targetPath); + LogUtils.d(TAG, "loadByBackgroundBean: 刷新缓存成功,路径=" + targetPath); + } else { + LogUtils.e(TAG, "loadByBackgroundBean: 刷新解码失败,路径=" + targetPath); } } + // 加载图片 loadImage(targetPath); + LogUtils.d(TAG, "=== loadByBackgroundBean 完成 ==="); } - // ====================================== 对外方法 ====================================== /** * 改进版:强制保持缓存策略,不自动清理任何缓存,强化引用计数管理,保留图片原始品质 * @param imagePath 图片绝对路径 */ public void loadImage(String imagePath) { LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ==="); + // 1. 路径空校验 if (TextUtils.isEmpty(imagePath)) { + LogUtils.e(TAG, "loadImage: 图片路径为空"); setDefaultTransparentBackground(); return; } - + // 2. 文件有效性校验 File imageFile = new File(imagePath); if (!imageFile.exists() || !imageFile.isFile()) { - LogUtils.e(TAG, "图片文件无效"); + LogUtils.e(TAG, "loadImage: 图片文件无效"); setDefaultTransparentBackground(); return; } - + // 3. 隐藏ImageView防止闪烁 mIvBackground.setVisibility(View.GONE); // ======================== 路径判断逻辑(强制缓存版) ======================== - // 1. 路径未变化:校验缓存有效性,无效则重加载(不删除旧缓存) + // 3.1 路径未变化:校验缓存有效性 if (imagePath.equals(mCurrentCachedPath)) { Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath); if (isBitmapValid(cachedBitmap)) { - LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存 Bitmap(原始品质)"); + LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存Bitmap(原始品质)"); mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight(); mIvBackground.setImageBitmap(cachedBitmap); adjustImageViewSize(); + LogUtils.d(TAG, "=== loadImage 完成(缓存命中) ==="); return; } else { - LogUtils.e(TAG, "loadImage: 缓存Bitmap无效,尝试重加载原始品质图片 - " + imagePath); - // 缓存无效,直接重加载原始品质图片(不删除旧缓存,强制保持策略) + LogUtils.e(TAG, "loadImage: 缓存Bitmap无效,尝试重加载原始品质图片"); } } - - // 2. 路径已更新:保留旧缓存,仅更新当前路径记录(核心修改:不删除旧缓存) + // 3.2 路径已更新:保留旧缓存,仅更新路径记录 if (!TextUtils.isEmpty(mCurrentCachedPath) && !mCurrentCachedPath.equals(imagePath)) { - LogUtils.d(TAG, "loadImage: 路径已更新,保留旧缓存,当前路径从 " + mCurrentCachedPath + " 切换到 " + imagePath); - // 仅更新当前路径记录,不删除旧缓存 + LogUtils.d(TAG, "loadImage: 路径已更新,保留旧缓存,原路径=" + mCurrentCachedPath + ",新路径=" + imagePath); } // ======================== 路径判断逻辑结束 ======================== - // 无缓存/缓存无效/路径更新:重新加载原始品质图片 + // 4. 计算图片宽高比 if (!calculateImageAspectRatio(imageFile)) { setDefaultTransparentBackground(); return; } - - // 先尝试从缓存获取 + // 5. 获取或解码Bitmap Bitmap bitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath); if (isBitmapValid(bitmap)) { - LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap(原始品质) - " + imagePath); + LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap(原始品质)"); } else { - // 缓存无效应解码原始品质图片 + LogUtils.d(TAG, "loadImage: 缓存未命中,解码原始品质图片"); bitmap = decodeOriginalBitmap(imageFile); if (bitmap == null) { LogUtils.e(TAG, "loadImage: 图片解码失败(原始品质)"); @@ -201,17 +213,15 @@ public class BackgroundView extends RelativeLayout { setDefaultTransparentBackground(); return; } - // 缓存新图片(强制保持,原始品质) + // 缓存新图片 App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap); - LogUtils.d(TAG, "loadImage: 加载新图片并缓存(原始品质) - " + imagePath); + LogUtils.d(TAG, "loadImage: 新图片缓存成功,路径=" + imagePath); } - - // 增加引用计数(配合全局缓存工具的引用计数机制) + // 6. 引用计数管理 App.sBitmapCacheUtils.increaseRefCount(imagePath); - // 更新当前缓存路径记录 + // 7. 更新当前缓存路径 mCurrentCachedPath = imagePath; - - // 直接使用setImageBitmap,避免BitmapDrawable包装的引用风险 + // 8. 设置图片并调整尺寸 mIvBackground.setImageBitmap(bitmap); adjustImageViewSize(); LogUtils.d(TAG, "=== loadImage 完成 ==="); @@ -222,27 +232,36 @@ public class BackgroundView extends RelativeLayout { * 工具方法:判断Bitmap是否有效(非空且未被回收) */ private boolean isBitmapValid(Bitmap bitmap) { - return bitmap != null && !bitmap.isRecycled(); + boolean valid = bitmap != null && !bitmap.isRecycled(); + if (!valid) { + LogUtils.w(TAG, "isBitmapValid: Bitmap无效(空或已回收)"); + } + return valid; } + /** + * 计算图片宽高比 + */ private boolean calculateImageAspectRatio(File file) { + LogUtils.d(TAG, "=== calculateImageAspectRatio 启动,文件=" + file.getAbsolutePath() + " ==="); try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(file.getAbsolutePath(), options); - + // 尺寸校验 int width = options.outWidth; int height = options.outHeight; if (width <= 0 || height <= 0) { - LogUtils.e(TAG, "图片尺寸无效"); + LogUtils.e(TAG, "calculateImageAspectRatio: 图片尺寸无效,宽=" + width + ",高=" + height); return false; } - + // 计算比例 mImageAspectRatio = (float) width / height; - LogUtils.d(TAG, "原图比例:" + mImageAspectRatio); + LogUtils.d(TAG, "calculateImageAspectRatio: 原图比例=" + mImageAspectRatio); + LogUtils.d(TAG, "=== calculateImageAspectRatio 完成 ==="); return true; } catch (Exception e) { - LogUtils.e(TAG, "计算比例失败:" + e.getMessage()); + LogUtils.e(TAG, "calculateImageAspectRatio: 计算比例失败:" + e.getMessage()); return false; } } @@ -251,33 +270,46 @@ public class BackgroundView extends RelativeLayout { * 移除压缩逻辑:解码原始品质图片(无缩放、无色彩损失) */ private Bitmap decodeOriginalBitmap(File file) { + LogUtils.d(TAG, "=== decodeOriginalBitmap 启动,文件=" + file.getAbsolutePath() + " ==="); try { BitmapFactory.Options options = new BitmapFactory.Options(); - // 核心配置:无缩放、全彩品质、关闭可回收标志(强制保持) - options.inSampleSize = 1; // 不缩放,采样率为1 - options.inPreferredConfig = Bitmap.Config.ARGB_8888; // 全彩品质,无色彩损失 - options.inPurgeable = false; // 关闭可回收标志,防止系统主动回收 + // 核心配置:原始品质 + options.inSampleSize = BITMAP_SAMPLE_SIZE; + options.inPreferredConfig = BITMAP_CONFIG; + options.inPurgeable = false; options.inInputShareable = false; - options.inDither = true; // 开启抖动,保证色彩还原精度 - options.inScaled = false; // 关闭自动缩放,保留原始尺寸 - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); + options.inDither = true; + options.inScaled = false; + // 解码图片 + Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options); + if (bitmap != null) { + LogUtils.d(TAG, "decodeOriginalBitmap: 解码成功,宽=" + bitmap.getWidth() + ",高=" + bitmap.getHeight()); + } else { + LogUtils.e(TAG, "decodeOriginalBitmap: 解码返回null"); + } + LogUtils.d(TAG, "=== decodeOriginalBitmap 完成 ==="); + return bitmap; } catch (Exception e) { - LogUtils.e(TAG, "原始品质解码失败:" + e.getMessage()); + LogUtils.e(TAG, "decodeOriginalBitmap: 原始品质解码失败:" + e.getMessage()); return null; } } + /** + * 调整ImageView尺寸,保持原图比例 + */ private void adjustImageViewSize() { + LogUtils.d(TAG, "=== adjustImageViewSize 启动 ==="); + // 空指针校验 if (mLlContainer == null || mIvBackground == null) { + LogUtils.e(TAG, "adjustImageViewSize: 容器或ImageView未初始化"); return; } - + // 获取容器尺寸 int llWidth = mLlContainer.getWidth(); int llHeight = mLlContainer.getHeight(); - if (llWidth == 0 || llHeight == 0) { LogUtils.w(TAG, "adjustImageViewSize: 容器尺寸未初始化,延迟调整"); - // 延迟调整(容器尺寸未就绪时) post(new Runnable() { @Override public void run() { @@ -286,7 +318,7 @@ public class BackgroundView extends RelativeLayout { }); return; } - + // 计算ImageView尺寸 int ivWidth, ivHeight; if (mImageAspectRatio >= 1.0f) { ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth); @@ -295,28 +327,36 @@ public class BackgroundView extends RelativeLayout { ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight); 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(ImageView.ScaleType.FIT_CENTER); mIvBackground.setVisibility(View.VISIBLE); + LogUtils.d(TAG, "adjustImageViewSize: 尺寸调整完成,宽=" + ivWidth + ",高=" + ivHeight); + LogUtils.d(TAG, "=== adjustImageViewSize 完成 ==="); } + /** + * 设置默认透明背景,仅减少引用计数,不删除缓存 + */ private void setDefaultTransparentBackground() { - // 清空ImageView的Drawable,释放本地引用(不影响全局缓存) + LogUtils.d(TAG, "=== setDefaultTransparentBackground 启动 ==="); + // 清空ImageView mIvBackground.setImageDrawable(null); mIvBackground.setBackgroundColor(0x00000000); mImageAspectRatio = 1.0f; - // 核心修改:路径清空时减少引用计数,不删除缓存 + // 减少引用计数,不删除缓存 if (!TextUtils.isEmpty(mCurrentCachedPath)) { + LogUtils.d(TAG, "setDefaultTransparentBackground: 减少引用计数,路径=" + mCurrentCachedPath); App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath); mCurrentCachedPath = ""; } + LogUtils.d(TAG, "=== setDefaultTransparentBackground 完成 ==="); } - // ====================================== 重写方法(核心改进) ====================================== + // ====================================== 重写生命周期方法 ====================================== /** * 重写:绘制前强制校验Bitmap有效性,防止已回收Bitmap崩溃 */ @@ -341,14 +381,16 @@ public class BackgroundView extends RelativeLayout { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - LogUtils.d(TAG, "onDetachedFromWindow: 减少引用计数,保留全局缓存"); + LogUtils.d(TAG, "=== onDetachedFromWindow 启动 ==="); // 清空ImageView的Drawable,释放本地引用 mIvBackground.setImageDrawable(null); - // 核心修改:仅减少引用计数,不删除全局缓存 + // 减少引用计数,不删除全局缓存 if (!TextUtils.isEmpty(mCurrentCachedPath)) { + LogUtils.d(TAG, "onDetachedFromWindow: 减少引用计数,路径=" + mCurrentCachedPath); App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath); mCurrentCachedPath = ""; } + LogUtils.d(TAG, "=== onDetachedFromWindow 完成 ==="); } /** @@ -357,7 +399,8 @@ public class BackgroundView extends RelativeLayout { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - adjustImageViewSize(); // 恢复尺寸调整 + LogUtils.d(TAG, "onSizeChanged: 尺寸变化,宽=" + w + ",高=" + h + ",调整ImageView尺寸"); + adjustImageViewSize(); } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BatteryDrawable.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BatteryDrawable.java index 2096456..51d3fca 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BatteryDrawable.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BatteryDrawable.java @@ -9,32 +9,37 @@ import android.graphics.drawable.Drawable; import cc.winboll.studio.libappbase.LogUtils; /** + * 电池电量Drawable:适配API30,兼容小米机型,支持能量/条纹两种绘制风格切换 * @Author ZhanGSKen&豆包大模型 * @Date 2025/12/17 12:55 - * @Describe 电池电量Drawable:适配API30,兼容小米机型,支持能量/条纹两种绘制风格切换 */ public class BatteryDrawable extends Drawable { - // ====================== 静态常量(置顶,按重要性排序) ====================== + // ====================================== 静态常量区(按功能归类,消除魔法值) ====================================== public static final String TAG = "BatteryDrawable"; // 小米机型绘制偏移校准(适配MIUI渲染特性,避免绘制错位) private static final int MIUI_DRAW_OFFSET = 1; // 默认电量透明度(兼顾显示效果与API30渲染性能) private static final int DEFAULT_BATTERY_ALPHA = 210; + // 电量范围常量 + private static final int BATTERY_MIN = 0; + private static final int BATTERY_MAX = 100; + // 条纹风格拆分数量 + private static final int STRIPE_COUNT = 100; - // ====================== 核心成员变量(按功能归类,final优先) ====================== + // ====================================== 成员变量区(final优先,按功能归类) ====================================== // 绘制画笔(final修饰,避免重复创建,提升性能) private final Paint mBatteryPaint; // 业务控制变量 private int mBatteryValue = -1; // 当前电量(0-100,-1=未初始化) private boolean mIsEnergyStyle = true; // 绘制风格(true=能量,false=条纹) - // ====================== 构造方法(重载适配,优先暴露常用构造) ====================== + // ====================================== 构造方法(重载适配,优先暴露常用构造) ====================================== /** * 构造方法(默认能量风格,常用场景) * @param batteryColor 电量显示颜色 */ public BatteryDrawable(int batteryColor) { - LogUtils.d(TAG, "constructor: 初始化(能量风格),颜色=" + Integer.toHexString(batteryColor)); + LogUtils.d(TAG, "【BatteryDrawable】构造器1调用 | 能量风格 | 颜色=" + Integer.toHexString(batteryColor)); mBatteryPaint = new Paint(); initPaintConfig(batteryColor); } @@ -45,85 +50,109 @@ public class BatteryDrawable extends Drawable { * @param isEnergyStyle 是否启用能量风格 */ public BatteryDrawable(int batteryColor, boolean isEnergyStyle) { - LogUtils.d(TAG, "constructor: 初始化,颜色=" + Integer.toHexString(batteryColor) + ",风格=" + (isEnergyStyle ? "能量" : "条纹")); + LogUtils.d(TAG, "【BatteryDrawable】构造器2调用 | 颜色=" + Integer.toHexString(batteryColor) + " | 风格=" + (isEnergyStyle ? "能量" : "条纹")); mBatteryPaint = new Paint(); mIsEnergyStyle = isEnergyStyle; initPaintConfig(batteryColor); } - // ====================== 私有初始化方法(封装复用,隐藏内部逻辑) ====================== + // ====================================== 私有初始化方法(封装复用,隐藏内部逻辑) ====================================== /** * 初始化画笔配置(适配API30渲染特性,优化小米机型兼容性) + * @param color 电量显示颜色 */ private void initPaintConfig(int color) { + LogUtils.d(TAG, "【initPaintConfig】画笔配置开始 | 颜色=" + Integer.toHexString(color)); mBatteryPaint.setColor(color); mBatteryPaint.setAlpha(DEFAULT_BATTERY_ALPHA); mBatteryPaint.setAntiAlias(true); // 抗锯齿,解决小米低分辨率锯齿问题 mBatteryPaint.setStyle(Paint.Style.FILL); // 固定填充模式,避免混乱 mBatteryPaint.setDither(false); // 禁用抖动,提升API30颜色显示一致性 - LogUtils.d(TAG, "initPaintConfig: 画笔配置完成"); + LogUtils.d(TAG, "【initPaintConfig】画笔配置完成"); } - // ====================== 核心绘制方法(Drawable抽象方法,优先级最高) ====================== + // ====================================== 核心绘制方法(Drawable抽象方法,优先级最高) ====================================== @Override public void draw(Canvas canvas) { + LogUtils.d(TAG, "【draw】绘制开始 | 当前电量=" + mBatteryValue + " | 风格=" + (mIsEnergyStyle ? "能量" : "条纹")); // 未初始化/异常电量,直接跳过,避免无效绘制 if (mBatteryValue < 0) { - LogUtils.w(TAG, "draw: 电量未初始化,跳过绘制"); + LogUtils.w(TAG, "【draw】电量未初始化,跳过绘制"); return; } // 强制校准电量范围(0-100),防止异常值导致绘制错误 - int validBattery = Math.max(0, Math.min(mBatteryValue, 100)); + int validBattery = Math.max(BATTERY_MIN, Math.min(mBatteryValue, BATTERY_MAX)); + LogUtils.d(TAG, "【draw】电量校准完成 | 有效电量=" + validBattery); + Rect drawBounds = getBounds(); + // 绘制边界空指针防护 + if (drawBounds == null) { + LogUtils.e(TAG, "【draw】绘制边界为空,跳过绘制"); + return; + } int drawHeight = drawBounds.height(); // 小米机型绘制偏移校准(解决MIUI系统渲染偏移问题) int offset = MIUI_DRAW_OFFSET; int left = drawBounds.left + offset; int right = drawBounds.right - offset; + LogUtils.d(TAG, "【draw】绘制参数校准 | 左边界=" + left + " | 右边界=" + right + " | 高度=" + drawHeight); - // 按风格执行绘制(精简日志,仅保留核心绘制参数) - LogUtils.d(TAG, "draw: 开始绘制,电量=" + validBattery + ",风格=" + (mIsEnergyStyle ? "能量" : "条纹")); + // 按风格执行绘制 if (mIsEnergyStyle) { drawEnergyStyle(canvas, validBattery, left, right, drawHeight); } else { drawStripeStyle(canvas, validBattery, left, right, drawHeight); } + LogUtils.d(TAG, "【draw】绘制完成"); } - // ====================== 绘制风格实现(私有封装,按风格拆分) ====================== + // ====================================== 绘制风格实现(私有封装,按风格拆分) ====================================== /** * 能量风格绘制(整块填充,高效简洁,默认风格) + * @param canvas 绘制画布 + * @param battery 有效电量(0-100) + * @param left 左边界 + * @param right 右边界 + * @param height 绘制高度 */ private void drawEnergyStyle(Canvas canvas, int battery, int left, int right, int height) { - int top = height - (height * battery / 100); // 计算电量对应顶部坐标 + LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制开始 | 电量=" + battery); + int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标 canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint); - LogUtils.d(TAG, "drawEnergyStyle: 绘制完成,顶部坐标=" + top); + LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top); } /** * 条纹风格绘制(分段条纹,扩展风格) + * @param canvas 绘制画布 + * @param battery 有效电量(0-100) + * @param left 左边界 + * @param right 右边界 + * @param height 绘制高度 */ private void drawStripeStyle(Canvas canvas, int battery, int left, int right, int height) { - int stripeHeight = height / 100; // 单条条纹高度(均匀拆分) + LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery); + int stripeHeight = height / STRIPE_COUNT; // 单条条纹高度(均匀拆分) // 从底部向上绘制对应电量条纹 for (int i = 0; i < battery; i++) { int bottom = height - (stripeHeight * i); int top = bottom - stripeHeight; canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint); } - LogUtils.d(TAG, "drawStripeStyle: 绘制完成,条纹数量=" + battery); + LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery); } - // ====================== 对外暴露方法(业务控制入口,按功能排序) ====================== + // ====================================== 对外暴露方法(业务控制入口,按功能排序) ====================================== /** * 设置当前电量(外部核心调用入口) * @param value 电量值(0-100) */ public void setBatteryValue(int value) { - LogUtils.d(TAG, "setBatteryValue: 电量更新,旧值=" + mBatteryValue + ",新值=" + value); + LogUtils.d(TAG, "【setBatteryValue】电量更新 | 旧值=" + mBatteryValue + " | 新值=" + value); mBatteryValue = value; invalidateSelf(); // 触发重绘,确保UI实时更新 + LogUtils.d(TAG, "【setBatteryValue】已触发重绘"); } /** @@ -131,9 +160,10 @@ public class BatteryDrawable extends Drawable { * @param isEnergyStyle true=能量风格,false=条纹风格 */ public void switchDrawStyle(boolean isEnergyStyle) { - LogUtils.d(TAG, "switchDrawStyle: 风格切换,旧=" + (mIsEnergyStyle ? "能量" : "条纹") + ",新=" + (isEnergyStyle ? "能量" : "条纹")); + LogUtils.d(TAG, "【switchDrawStyle】风格切换 | 旧风格=" + (mIsEnergyStyle ? "能量" : "条纹") + " | 新风格=" + (isEnergyStyle ? "能量" : "条纹")); mIsEnergyStyle = isEnergyStyle; invalidateSelf(); + LogUtils.d(TAG, "【switchDrawStyle】已触发重绘"); } /** @@ -141,31 +171,42 @@ public class BatteryDrawable extends Drawable { * @param color 新颜色值 */ public void updateBatteryColor(int color) { - LogUtils.d(TAG, "updateBatteryColor: 颜色更新,旧=" + Integer.toHexString(mBatteryPaint.getColor()) + ",新=" + Integer.toHexString(color)); + String oldColor = Integer.toHexString(mBatteryPaint.getColor()); + String newColor = Integer.toHexString(color); + LogUtils.d(TAG, "【updateBatteryColor】颜色更新 | 旧颜色=" + oldColor + " | 新颜色=" + newColor); mBatteryPaint.setColor(color); invalidateSelf(); + LogUtils.d(TAG, "【updateBatteryColor】已触发重绘"); } - // ====================== Getter方法(按需暴露,简洁无冗余) ====================== + // ====================================== Getter方法(按需暴露,简洁无冗余) ====================================== + /** + * 获取当前电量 + * @return 电量值(0-100,-1=未初始化) + */ public int getBatteryValue() { return mBatteryValue; } + /** + * 获取当前绘制风格 + * @return true=能量风格,false=条纹风格 + */ public boolean isEnergyStyle() { return mIsEnergyStyle; } - // ====================== Drawable抽象方法(必须实现,精简逻辑) ====================== + // ====================================== Drawable抽象方法(必须实现,精简逻辑) ====================================== @Override public void setAlpha(int alpha) { - LogUtils.d(TAG, "setAlpha: 透明度更新,旧=" + mBatteryPaint.getAlpha() + ",新=" + alpha); + LogUtils.d(TAG, "【setAlpha】透明度更新 | 旧值=" + mBatteryPaint.getAlpha() + " | 新值=" + alpha); mBatteryPaint.setAlpha(alpha); invalidateSelf(); } @Override public void setColorFilter(ColorFilter colorFilter) { - LogUtils.d(TAG, "setColorFilter: 设置颜色过滤,filter=" + colorFilter); + LogUtils.d(TAG, "【setColorFilter】设置颜色过滤 | filter=" + colorFilter); mBatteryPaint.setColorFilter(colorFilter); invalidateSelf(); } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MainContentView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MainContentView.java index 3107a41..2a17c8d 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MainContentView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MainContentView.java @@ -22,24 +22,26 @@ import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.utils.AppConfigUtils; /** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/12/17 13:14 - * @Describe 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑 + * 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑 * 适配:Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框 * 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值 + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/17 13:14 */ public class MainContentView { - // ======================== 静态常量(置顶,唯一标识)======================== + // ====================================== 静态常量区(唯一标识,变更类型分类) ====================================== public static final String TAG = "MainContentView"; - // 变更类型常量(区分不同控件,精准处理逻辑) private static final int CHANGE_TYPE_CHARGE_SWITCH = 1; private static final int CHANGE_TYPE_USAGE_SWITCH = 2; private static final int CHANGE_TYPE_SERVICE_SWITCH = 3; private static final int CHANGE_TYPE_CHARGE_SEEKBAR = 4; private static final int CHANGE_TYPE_USAGE_SEEKBAR = 5; + // 电量范围常量 + private static final int BATTERY_MIN = 0; + private static final int BATTERY_MAX = 100; - // ======================== 内部静态类(临时数据载体,避免外部依赖)======================== + // ====================================== 内部静态类(临时数据载体,避免外部依赖) ====================================== /** * 临时配置数据实体(缓存变更信息,取消时恢复) */ @@ -65,7 +67,7 @@ public class MainContentView { } } - // ======================== 事件回调接口(解耦视图与业务,提升扩展性)======================== + // ====================================== 事件回调接口(解耦视图与业务,提升扩展性) ====================================== public interface OnViewActionListener { void onChargeReminderSwitchChanged(boolean isChecked); void onUsageReminderSwitchChanged(boolean isChecked); @@ -74,17 +76,17 @@ public class MainContentView { void onUsageReminderProgressChanged(int progress); } - // ======================== 成员变量(按功能分类,避免混乱)======================== + // ====================================== 成员变量区(按功能分类,final优先,避免混乱) ====================================== // 外部依赖实例(生命周期关联,优先声明) private Context mContext; private AppConfigUtils mAppConfigUtils; private OnViewActionListener mActionListener; - // 视图控件(按「布局→开关→文本→进度条→图标」功能归类) + // 视图控件(按「布局→开关→文本→进度条→图标」功能归类,public控件标注用途) // 基础布局控件 public RelativeLayout mainLayout; public MemoryCachedBackgroundView backgroundView; - LinearLayout mllBackgroundView; + private LinearLayout mllBackgroundView; // 容器布局控件 public LinearLayout llLeftSeekBar; public LinearLayout llRightSeekBar; @@ -122,9 +124,9 @@ public class MainContentView { // 对话框状态锁(避免快速点击重复弹窗) private boolean isDialogShowing = false; - // ======================== 构造方法(初始化入口,逻辑闭环)======================== + // ====================================== 构造方法(初始化入口,逻辑闭环) ====================================== public MainContentView(Context context, View rootView, OnViewActionListener actionListener) { - LogUtils.d(TAG, "MainContentView() | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener); + LogUtils.d(TAG, "【MainContentView】构造器调用 | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener); // 初始化外部依赖 this.mContext = context; this.mActionListener = actionListener; @@ -136,24 +138,25 @@ public class MainContentView { initConfirmDialog(); bindViewListeners(); - LogUtils.d(TAG, "MainContentView 初始化完成"); + LogUtils.d(TAG, "【MainContentView】初始化完成"); } - // ======================== 私有初始化方法(封装内部逻辑,仅暴露入口)======================== + // ====================================== 私有初始化方法(封装内部逻辑,仅暴露入口) ====================================== /** * 绑定视图控件(显式强转适配 Java7,适配 API30 视图加载机制) + * @param rootView 根视图 */ private void bindViews(View rootView) { - LogUtils.d(TAG, "bindViews() | rootView=" + rootView); + LogUtils.d(TAG, "【bindViews】视图绑定开始 | rootView=" + rootView); // 基础布局绑定 mainLayout = (RelativeLayout) rootView.findViewById(R.id.activitymainRelativeLayout1); - //backgroundView = (BackgroundView) rootView.findViewById(R.id.fragmentmainviewBackgroundView1); mllBackgroundView = (LinearLayout) rootView.findViewById(R.id.ll_backgroundview); - backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext); - if (backgroundView.getParent() != null) { - ((ViewGroup) backgroundView.getParent()).removeView(backgroundView); - } - mllBackgroundView.addView(backgroundView); + backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext); + if (backgroundView.getParent() != null) { + ((ViewGroup) backgroundView.getParent()).removeView(backgroundView); + LogUtils.d(TAG, "【bindViews】移除背景视图旧父容器"); + } + mllBackgroundView.addView(backgroundView); // 容器布局绑定 llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1); llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2); @@ -177,17 +180,19 @@ public class MainContentView { // 初始化进度缓存(从配置读取初始值) mCurrentChargeProgress = mAppConfigUtils.getChargeReminderValue(); mCurrentUsageProgress = mAppConfigUtils.getUsageReminderValue(); + LogUtils.d(TAG, "【bindViews】进度缓存初始化 | charge=" + mCurrentChargeProgress + " | usage=" + mCurrentUsageProgress); // 关键视图绑定校验(仅保留核心控件错误日志,精简冗余) - if (mainLayout == null) LogUtils.e(TAG, "mainLayout 绑定失败"); - if (backgroundView == null) LogUtils.e(TAG, "backgroundView 绑定失败"); + if (mainLayout == null) LogUtils.e(TAG, "【bindViews】mainLayout 绑定失败"); + if (backgroundView == null) LogUtils.e(TAG, "【bindViews】backgroundView 绑定失败"); + LogUtils.d(TAG, "【bindViews】视图绑定完成"); } /** * 初始化电池 Drawable(集成 BatteryDrawable,默认能量风格,适配小米机型渲染) */ private void initBatteryDrawables() { - LogUtils.d(TAG, "initBatteryDrawables()"); + LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化开始"); // 当前电量 Drawable(颜色从资源读取,适配 API30 主题) int colorCurrent = getResourceColor(R.color.colorCurrent); mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent); @@ -197,15 +202,16 @@ public class MainContentView { // 耗电提醒 Drawable int colorUsage = getResourceColor(R.color.colorUsege); mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage); + LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成"); } /** * 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题) */ private void initConfirmDialog() { - LogUtils.d(TAG, "initConfirmDialog()"); + LogUtils.d(TAG, "【initConfirmDialog】对话框初始化开始"); if (mContext == null) { - LogUtils.e(TAG, "Context 为空,初始化失败"); + LogUtils.e(TAG, "【initConfirmDialog】Context 为空,初始化失败"); return; } @@ -245,16 +251,17 @@ public class MainContentView { mConfigConfirmDialog = mDialogBuilder.create(); mConfigConfirmDialog.setCancelable(true); mConfigConfirmDialog.setCanceledOnTouchOutside(true); + LogUtils.d(TAG, "【initConfirmDialog】对话框初始化完成"); } /** * 绑定视图事件监听(Java7 显式实现接口,适配 API30 事件分发,修复进度条弹窗失效) */ private void bindViewListeners() { - LogUtils.d(TAG, "bindViewListeners()"); + LogUtils.d(TAG, "【bindViewListeners】事件监听绑定开始"); // 依赖校验,避免空指针 - if (mAppConfigUtils == null || mActionListener == null || mConfigConfirmDialog == null) { - LogUtils.e(TAG, "依赖实例为空,跳过监听绑定"); + if (mAppConfigUtils == null || mActionListener == null || mDialogBuilder == null) { + LogUtils.e(TAG, "【bindViewListeners】依赖实例为空,跳过监听绑定"); return; } @@ -267,14 +274,14 @@ public class MainContentView { int originalValue = mAppConfigUtils.getChargeReminderValue(); // 进度无变化,不处理 if (originalValue == progress) { - LogUtils.d(TAG, "ChargeReminderSeekBar: 进度无变化,跳过"); + LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar: 进度无变化,跳过"); return; } // 缓存变更数据,显示确认对话框 mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SEEKBAR, originalValue, progress); updateDialogMessageByChangeType(); showConfigConfirmDialog(); - LogUtils.d(TAG, "ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress); + LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress); } @Override @@ -289,7 +296,7 @@ public class MainContentView { seekBar.setProgress(originalValue); // 恢复进度缓存 mCurrentChargeProgress = originalValue; - LogUtils.d(TAG, "ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue); + LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue); } }); @@ -305,6 +312,7 @@ public class MainContentView { ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable); tvChargeReminderValue.setText(progress + "%"); } + LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar实时更新 | 进度=" + progress); } } @@ -314,7 +322,7 @@ public class MainContentView { @Override public void onStopTrackingTouch(VerticalSeekBar seekBar) {} }); - LogUtils.d(TAG, "充电提醒进度条专属监听绑定完成"); + LogUtils.d(TAG, "【bindViewListeners】充电提醒进度条专属监听绑定完成"); } // 充电提醒开关监听 @@ -330,10 +338,10 @@ public class MainContentView { mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SWITCH, originalValue, newValue); updateDialogMessageByChangeType(); showConfigConfirmDialog(); - LogUtils.d(TAG, "cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue); + LogUtils.d(TAG, "【bindViewListeners】cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue); } }); - LogUtils.d(TAG, "充电提醒开关监听绑定完成"); + LogUtils.d(TAG, "【bindViewListeners】充电提醒开关监听绑定完成"); } // 耗电提醒进度条监听(使用 VerticalSeekBar 专属接口,确保弹窗100%触发) @@ -345,14 +353,14 @@ public class MainContentView { int originalValue = mAppConfigUtils.getUsageReminderValue(); // 进度无变化,不处理 if (originalValue == progress) { - LogUtils.d(TAG, "UsageReminderSeekBar: 进度无变化,跳过"); + LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar: 进度无变化,跳过"); return; } // 缓存变更数据,显示确认对话框 mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SEEKBAR, originalValue, progress); updateDialogMessageByChangeType(); showConfigConfirmDialog(); - LogUtils.d(TAG, "UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress); + LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress); } @Override @@ -367,7 +375,7 @@ public class MainContentView { seekBar.setProgress(originalValue); // 恢复进度缓存 mCurrentUsageProgress = originalValue; - LogUtils.d(TAG, "UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue); + LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue); } }); @@ -383,6 +391,7 @@ public class MainContentView { ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable); tvUsageReminderValue.setText(progress + "%"); } + LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar实时更新 | 进度=" + progress); } } @@ -392,7 +401,7 @@ public class MainContentView { @Override public void onStopTrackingTouch(VerticalSeekBar seekBar) {} }); - LogUtils.d(TAG, "耗电提醒进度条专属监听绑定完成"); + LogUtils.d(TAG, "【bindViewListeners】耗电提醒进度条专属监听绑定完成"); } // 耗电提醒开关监听 @@ -408,10 +417,10 @@ public class MainContentView { mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SWITCH, originalValue, newValue); updateDialogMessageByChangeType(); showConfigConfirmDialog(); - LogUtils.d(TAG, "cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue); + LogUtils.d(TAG, "【bindViewListeners】cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue); } }); - LogUtils.d(TAG, "耗电提醒开关监听绑定完成"); + LogUtils.d(TAG, "【bindViewListeners】耗电提醒开关监听绑定完成"); } // 服务总开关监听(核心优化:逻辑与其他控件完全对齐) @@ -430,24 +439,24 @@ public class MainContentView { updateDialogMessageByChangeType(); // 显示确认对话框 showConfigConfirmDialog(); - LogUtils.d(TAG, "swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue); + LogUtils.d(TAG, "【bindViewListeners】swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue); } }); - LogUtils.d(TAG, "服务总开关监听绑定完成"); + LogUtils.d(TAG, "【bindViewListeners】服务总开关监听绑定完成"); } - LogUtils.d(TAG, "所有事件监听绑定完成"); + LogUtils.d(TAG, "【bindViewListeners】所有事件监听绑定完成"); } - // ======================== 对外暴露核心方法(业务入口,精简参数,明确职责)======================== + // ====================================== 对外暴露核心方法(业务入口,精简参数,明确职责) ====================================== /** * 更新所有视图数据(从配置读取数据,统一刷新 UI,适配 API30 视图更新规范) * @param frameDrawable 进度条背景 Drawable(外部传入,适配主题切换) */ public void updateViewData(Drawable frameDrawable) { - LogUtils.d(TAG, "updateViewData() | frameDrawable=" + frameDrawable); + LogUtils.d(TAG, "【updateViewData】视图数据更新开始 | frameDrawable=" + frameDrawable); if (mAppConfigUtils == null) { - LogUtils.e(TAG, "AppConfigUtils 为空,跳过更新"); + LogUtils.e(TAG, "【updateViewData】AppConfigUtils 为空,跳过更新"); return; } @@ -462,12 +471,13 @@ public class MainContentView { // 更新进度缓存 mCurrentChargeProgress = chargeVal; mCurrentUsageProgress = usageVal; - LogUtils.d(TAG, "配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable); + LogUtils.d(TAG, "【updateViewData】配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable); // 进度条背景更新 if (frameDrawable != null) { if (llLeftSeekBar != null) llLeftSeekBar.setBackground(frameDrawable); if (llRightSeekBar != null) llRightSeekBar.setBackground(frameDrawable); + LogUtils.d(TAG, "【updateViewData】进度条背景更新完成"); } // 当前电量更新(联动 BatteryDrawable,实时刷新图标) @@ -479,6 +489,7 @@ public class MainContentView { tvCurrentBatteryValue.setTextColor(getResourceColor(R.color.colorCurrent)); tvCurrentBatteryValue.setText(currentVal + "%"); } + LogUtils.d(TAG, "【updateViewData】当前电量更新完成"); // 充电提醒视图更新 if (ivChargeReminderBattery != null && mChargeReminderBatteryDrawable != null) { @@ -491,6 +502,7 @@ public class MainContentView { } if (sbChargeReminder != null) sbChargeReminder.setProgress(chargeVal); if (cbEnableChargeReminder != null) cbEnableChargeReminder.setChecked(chargeEnable); + LogUtils.d(TAG, "【updateViewData】充电提醒视图更新完成"); // 耗电提醒视图更新 if (ivUsageReminderBattery != null && mUsageReminderBatteryDrawable != null) { @@ -503,6 +515,7 @@ public class MainContentView { } if (sbUsageReminder != null) sbUsageReminder.setProgress(usageVal); if (cbEnableUsageReminder != null) cbEnableUsageReminder.setChecked(usageEnable); + LogUtils.d(TAG, "【updateViewData】耗电提醒视图更新完成"); // 服务开关+提示文本更新(确保状态准确) if (swEnableService != null) { @@ -510,8 +523,9 @@ public class MainContentView { swEnableService.setText(mContext.getString(R.string.txt_aboveswitch)); } if (tvTips != null) tvTips.setText(mContext.getString(R.string.txt_aboveswitchtips)); + LogUtils.d(TAG, "【updateViewData】服务开关与提示文本更新完成"); - LogUtils.d(TAG, "所有视图数据更新完成"); + LogUtils.d(TAG, "【updateViewData】所有视图数据更新完成"); } /** @@ -519,28 +533,28 @@ public class MainContentView { * @param value 电量值(自动校准 0-100,避免异常值) */ public void updateCurrentBattery(int value) { - LogUtils.d(TAG, "updateCurrentBattery() | 原始值=" + value); + LogUtils.d(TAG, "【updateCurrentBattery】当前电量更新开始 | 原始值=" + value); // 核心依赖校验 if (tvCurrentBatteryValue == null || mCurrentBatteryDrawable == null || ivCurrentBattery == null) { - LogUtils.e(TAG, "视图/Drawable 为空,跳过更新"); + LogUtils.e(TAG, "【updateCurrentBattery】视图/Drawable 为空,跳过更新"); return; } // 校准电量范围(强制 0-100,防止 API30 视图显示异常) - int validValue = Math.max(0, Math.min(value, 100)); + int validValue = Math.max(BATTERY_MIN, Math.min(value, BATTERY_MAX)); // 联动 BatteryDrawable 更新图标,同步文本显示 mCurrentBatteryDrawable.setBatteryValue(validValue); ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable); tvCurrentBatteryValue.setText(validValue + "%"); - LogUtils.d(TAG, "更新完成 | 校准后值=" + validValue); + LogUtils.d(TAG, "【updateCurrentBattery】更新完成 | 校准后值=" + validValue); } /** * 释放资源(主动回收,适配 API30 资源管控机制,优化小米手机内存占用) */ public void releaseResources() { - LogUtils.d(TAG, "releaseResources()"); + LogUtils.d(TAG, "【releaseResources】资源释放开始"); // 释放对话框资源(安全销毁,避免内存泄漏) if (mConfigConfirmDialog != null) { if (mConfigConfirmDialog.isShowing()) { @@ -563,6 +577,7 @@ public class MainContentView { // 置空视图实例(断开视图引用,辅助 GC 回收) mainLayout = null; backgroundView = null; + mllBackgroundView = null; llLeftSeekBar = null; llRightSeekBar = null; cbEnableChargeReminder = null; @@ -583,7 +598,7 @@ public class MainContentView { mAppConfigUtils = null; mActionListener = null; - LogUtils.d(TAG, "所有资源释放完成"); + LogUtils.d(TAG, "【releaseResources】所有资源释放完成"); } /** @@ -591,7 +606,7 @@ public class MainContentView { * @param enabled 服务启用状态 */ public void setServiceSwitchChecked(boolean enabled) { - LogUtils.d(TAG, "setServiceSwitchChecked() | enabled=" + enabled); + LogUtils.d(TAG, "【setServiceSwitchChecked】服务开关状态设置 | enabled=" + enabled); if (swEnableService != null) { swEnableService.setChecked(enabled); } @@ -602,33 +617,33 @@ public class MainContentView { * @param enabled 是否允许点击 */ public void setServiceSwitchEnabled(boolean enabled) { - LogUtils.d(TAG, "setServiceSwitchEnabled() | enabled=" + enabled); + LogUtils.d(TAG, "【setServiceSwitchEnabled】服务开关点击状态设置 | enabled=" + enabled); if (swEnableService != null) { swEnableService.setEnabled(enabled); } } - // ======================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑)======================== + // ====================================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑) ====================================== /** * 显示配置变更确认对话框(确保 Activity 处于前台,避免异常,防止重复弹窗) */ private void showConfigConfirmDialog() { - LogUtils.d(TAG, "showConfigConfirmDialog() | isDialogShowing=" + isDialogShowing); + LogUtils.d(TAG, "【showConfigConfirmDialog】对话框显示开始 | isDialogShowing=" + isDialogShowing); // 对话框状态锁:正在显示则跳过,避免重复触发 if (isDialogShowing) { - LogUtils.d(TAG, "对话框已显示,跳过重复调用"); + LogUtils.d(TAG, "【showConfigConfirmDialog】对话框已显示,跳过重复调用"); return; } // 基础校验:对话框/上下文/Builder 为空 if (mDialogBuilder == null || mContext == null) { - LogUtils.e(TAG, "对话框Builder/上下文异常,无法显示"); + LogUtils.e(TAG, "【showConfigConfirmDialog】对话框Builder/上下文异常,无法显示"); if (mTempConfigData != null) cancelConfigChange(); return; } // Activity 状态校验:避免销毁后弹窗崩溃(适配 API30) Activity activity = (Activity) mContext; if (activity.isFinishing() || activity.isDestroyed()) { - LogUtils.e(TAG, "Activity 已销毁,无法显示对话框"); + LogUtils.e(TAG, "【showConfigConfirmDialog】Activity 已销毁,无法显示对话框"); if (mTempConfigData != null) cancelConfigChange(); return; } @@ -645,16 +660,16 @@ public class MainContentView { mConfigConfirmDialog.setOnDismissListener(null); } }); - LogUtils.d(TAG, "确认对话框显示成功"); + LogUtils.d(TAG, "【showConfigConfirmDialog】确认对话框显示成功"); } /** * 确认配置变更(保存数据+回调监听+更新视图) */ private void confirmConfigChange() { - LogUtils.d(TAG, "confirmConfigChange() | mTempConfigData=" + mTempConfigData); + LogUtils.d(TAG, "【confirmConfigChange】配置确认开始 | mTempConfigData=" + mTempConfigData); if (mTempConfigData == null || mAppConfigUtils == null || mActionListener == null) { - LogUtils.e(TAG, "依赖数据为空,确认失败"); + LogUtils.e(TAG, "【confirmConfigChange】依赖数据为空,确认失败"); return; } @@ -663,13 +678,13 @@ public class MainContentView { case CHANGE_TYPE_CHARGE_SWITCH: mAppConfigUtils.setChargeReminderEnabled(mTempConfigData.newBooleanValue); mActionListener.onChargeReminderSwitchChanged(mTempConfigData.newBooleanValue); - LogUtils.d(TAG, "充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue); + LogUtils.d(TAG, "【confirmConfigChange】充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue); break; // 耗电提醒开关 case CHANGE_TYPE_USAGE_SWITCH: mAppConfigUtils.setUsageReminderEnabled(mTempConfigData.newBooleanValue); mActionListener.onUsageReminderSwitchChanged(mTempConfigData.newBooleanValue); - LogUtils.d(TAG, "耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue); + LogUtils.d(TAG, "【confirmConfigChange】耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue); break; // 服务总开关(核心:持久化配置+触发 Activity 回调) case CHANGE_TYPE_SERVICE_SWITCH: @@ -681,36 +696,37 @@ public class MainContentView { } // 2. 强制触发 Activity 回调,执行服务启停逻辑 mActionListener.onServiceSwitchChanged(mTempConfigData.newBooleanValue); - LogUtils.d(TAG, "服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置"); + LogUtils.d(TAG, "【confirmConfigChange】服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置"); break; // 充电提醒进度条 case CHANGE_TYPE_CHARGE_SEEKBAR: mAppConfigUtils.setChargeReminderValue(mTempConfigData.newIntValue); mActionListener.onChargeReminderProgressChanged(mTempConfigData.newIntValue); - LogUtils.d(TAG, "充电提醒进度确认 | 值=" + mTempConfigData.newIntValue); + LogUtils.d(TAG, "【confirmConfigChange】充电提醒进度确认 | 值=" + mTempConfigData.newIntValue); break; // 耗电提醒进度条 case CHANGE_TYPE_USAGE_SEEKBAR: mAppConfigUtils.setUsageReminderValue(mTempConfigData.newIntValue); mActionListener.onUsageReminderProgressChanged(mTempConfigData.newIntValue); - LogUtils.d(TAG, "耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue); + LogUtils.d(TAG, "【confirmConfigChange】耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue); break; default: - LogUtils.w(TAG, "未知变更类型,跳过"); + LogUtils.w(TAG, "【confirmConfigChange】未知变更类型,跳过"); break; } // 确认完成,清空临时数据 mTempConfigData = null; + LogUtils.d(TAG, "【confirmConfigChange】配置确认完成"); } /** * 取消配置变更(恢复原始值+刷新视图,确保 UI 与配置一致) */ private void cancelConfigChange() { - LogUtils.d(TAG, "cancelConfigChange() | mTempConfigData=" + mTempConfigData); + LogUtils.d(TAG, "【cancelConfigChange】配置取消开始 | mTempConfigData=" + mTempConfigData); if (mTempConfigData == null || mAppConfigUtils == null) { - LogUtils.e(TAG, "依赖数据为空,取消失败"); + LogUtils.e(TAG, "【cancelConfigChange】依赖数据为空,取消失败"); return; } @@ -719,19 +735,19 @@ public class MainContentView { if (cbEnableChargeReminder != null) { cbEnableChargeReminder.setChecked(mTempConfigData.originalBooleanValue); } - LogUtils.d(TAG, "充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); + LogUtils.d(TAG, "【cancelConfigChange】充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); break; case CHANGE_TYPE_USAGE_SWITCH: if (cbEnableUsageReminder != null) { cbEnableUsageReminder.setChecked(mTempConfigData.originalBooleanValue); } - LogUtils.d(TAG, "耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); + LogUtils.d(TAG, "【cancelConfigChange】耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); break; case CHANGE_TYPE_SERVICE_SWITCH: if (swEnableService != null) { swEnableService.setChecked(mTempConfigData.originalBooleanValue); } - LogUtils.d(TAG, "服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); + LogUtils.d(TAG, "【cancelConfigChange】服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue); break; case CHANGE_TYPE_CHARGE_SEEKBAR: if (sbChargeReminder != null) { @@ -742,7 +758,7 @@ public class MainContentView { ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable); tvChargeReminderValue.setText(mTempConfigData.originalIntValue + "%"); } - LogUtils.d(TAG, "充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue); + LogUtils.d(TAG, "【cancelConfigChange】充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue); break; case CHANGE_TYPE_USAGE_SEEKBAR: if (sbUsageReminder != null) { @@ -753,22 +769,23 @@ public class MainContentView { ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable); tvUsageReminderValue.setText(mTempConfigData.originalIntValue + "%"); } - LogUtils.d(TAG, "耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue); + LogUtils.d(TAG, "【cancelConfigChange】耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue); break; default: - LogUtils.w(TAG, "未知变更类型,跳过"); + LogUtils.w(TAG, "【cancelConfigChange】未知变更类型,跳过"); break; } // 取消完成,清空临时数据 mTempConfigData = null; + LogUtils.d(TAG, "【cancelConfigChange】配置取消完成"); } /** * 根据变更类型更新对话框提示语(核心优化:通过 Builder 更新,确保生效) */ private void updateDialogMessageByChangeType() { - LogUtils.d(TAG, "updateDialogMessageByChangeType() | mTempConfigData=" + mTempConfigData); + LogUtils.d(TAG, "【updateDialogMessageByChangeType】对话框消息更新开始 | mTempConfigData=" + mTempConfigData); if (mDialogBuilder == null || mTempConfigData == null) return; String message; if (mTempConfigData.changeType == CHANGE_TYPE_SERVICE_SWITCH) { @@ -782,42 +799,20 @@ public class MainContentView { } // 通过 Builder 设置消息,确保弹窗显示最新内容 mDialogBuilder.setMessage(message); + LogUtils.d(TAG, "【updateDialogMessageByChangeType】对话框消息更新完成 | message=" + message); } - // ======================== 内部工具方法(封装重复逻辑,提升复用性)======================== - /** - * 实时计算并更新比值预览(sbUsageReminder / sbChargeReminder) - * 处理除数为0的情况,避免崩溃 - */ -// private void updateRatioPreview() { -// if (mTvRatioPreview == null) return; -// float ratio; -// // 处理除数为0:充电进度为0时显示0(可根据需求改为“--”) -// if (mCurrentChargeProgress == 0) { -// ratio = 0.0f; -// } else { -// ratio = (float) mCurrentUsageProgress / mCurrentChargeProgress; -// } -// // 格式化比值:保留1位小数,适配本地化(解决小米手机小数分隔符问题) -// String ratioText = String.format(Locale.getDefault(), "比值:%.1f", ratio); -// mTvRatioPreview.setText(ratioText); -// // 触发比值变化回调 -// if (mActionListener != null) { -// mActionListener.onRatioChanged(ratio); -// } -// LogUtils.d(TAG, "比值预览更新 | usage=" + mCurrentUsageProgress + " | charge=" + mCurrentChargeProgress + " | ratio=" + ratio); -// } - + // ====================================== 内部工具方法(封装重复逻辑,提升复用性) ====================================== /** * 获取资源颜色(适配 API30 主题颜色读取机制,兼容低版本,优化小米机型颜色显示,防御空指针) * @param colorResId 颜色资源 ID * @return 校准后的颜色值 */ private int getResourceColor(int colorResId) { - LogUtils.d(TAG, "getResourceColor() | colorResId=" + colorResId); + LogUtils.d(TAG, "【getResourceColor】资源颜色获取 | colorResId=" + colorResId); // 空指针防御:Context 为空返回默认黑色 if (mContext == null) { - LogUtils.e(TAG, "Context 为空,返回默认黑色"); + LogUtils.e(TAG, "【getResourceColor】Context 为空,返回默认黑色"); return 0xFF000000; } // 适配 API30 主题颜色读取 @@ -833,11 +828,11 @@ public class MainContentView { * @return 服务启用状态(true=启用,false=禁用) */ private boolean getServiceEnableState() { - LogUtils.d(TAG, "getServiceEnableState()"); + LogUtils.d(TAG, "【getServiceEnableState】服务状态获取开始"); ControlCenterServiceBean serviceBean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class); // 本地无配置时,默认禁用服务(与服务初始化逻辑对齐) boolean state = serviceBean != null && serviceBean.isEnableService(); - LogUtils.d(TAG, "服务启用状态获取完成 | state=" + state); + LogUtils.d(TAG, "【getServiceEnableState】服务启用状态获取完成 | state=" + state); return state; } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MemoryCachedBackgroundView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MemoryCachedBackgroundView.java index 1743d6a..0af5d7e 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MemoryCachedBackgroundView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/MemoryCachedBackgroundView.java @@ -8,41 +8,44 @@ import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.powerbell.models.BackgroundBean; /** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/12/21 20:43 - * @Describe 单实例缓存版背景视图控件(基于Java7)- 强制缓存版 + * 单实例缓存版背景视图控件(基于Java7)- 强制缓存版 * 核心:通过静态属性保存当前缓存路径和实例,支持强制重载图片 * 新增:SP持久化最后加载路径、获取最后加载实例功能 * 强制缓存策略:无论内存是否紧张,不自动清理任何缓存实例和路径记录 + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/21 20:43 */ public class MemoryCachedBackgroundView extends BackgroundView { + // ====================================== 静态常量区(TAG + SP相关常量) ====================================== public static final String TAG = "MemoryCachedBackgroundView"; + // SP相关常量(持久化最后加载路径) + private static final String SP_NAME = "MemoryCachedBackgroundView_SP"; + private static final String KEY_LAST_LOAD_IMAGE_PATH = "last_load_image_path"; + + // ====================================== 静态属性区(强制缓存核心:保存实例、路径、实例计数) ====================================== // 静态属性:保存当前缓存的路径和实例(强制保持,不自动销毁) private static String sCachedImagePath; private static MemoryCachedBackgroundView sCachedView; // 新增:记录所有创建过的实例数量(用于强制缓存监控) private static int sInstanceCount = 0; - // SP相关常量 - private static final String SP_NAME = "MemoryCachedBackgroundView_SP"; - private static final String KEY_LAST_LOAD_IMAGE_PATH = "last_load_image_path"; - // ====================================== 构造器(继承并兼容父类) ====================================== + // ====================================== 构造器(继承并兼容父类,私有构造防止外部实例化) ====================================== private MemoryCachedBackgroundView(Context context) { super(context); sInstanceCount++; - LogUtils.d(TAG, "构造器1:创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount); + LogUtils.d(TAG, "【构造器1】创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount); } private MemoryCachedBackgroundView(Context context, AttributeSet attrs) { super(context, attrs); sInstanceCount++; - LogUtils.d(TAG, "构造器2:创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount); + LogUtils.d(TAG, "【构造器2】创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount); } private MemoryCachedBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); sInstanceCount++; - LogUtils.d(TAG, "构造器3:创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount); + LogUtils.d(TAG, "【构造器3】创建MemoryCachedBackgroundView实例,当前实例总数:" + sInstanceCount); } // ====================================== 核心静态方法:获取/创建缓存实例(强制缓存版) ====================================== @@ -54,32 +57,32 @@ public class MemoryCachedBackgroundView extends BackgroundView { * @return 缓存/新创建的MemoryCachedBackgroundView实例 */ public static MemoryCachedBackgroundView getInstance(Context context, String imagePath, boolean isReload) { - LogUtils.d(TAG, "getInstance() 调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload + " | 当前实例总数:" + sInstanceCount); + LogUtils.d(TAG, "【getInstance】调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload); + // 空路径校验 if (TextUtils.isEmpty(imagePath)) { - LogUtils.e(TAG, "getInstance():图片路径为空,创建空实例"); + LogUtils.e(TAG, "【getInstance】图片路径为空,创建空实例"); return new MemoryCachedBackgroundView(context); } // 1. 路径匹配缓存 → 判断是否强制重载 if (imagePath.equals(sCachedImagePath) && sCachedView != null) { - LogUtils.d(TAG, "getInstance():路径已缓存,当前缓存实例有效"); + LogUtils.d(TAG, "【getInstance】路径已缓存,当前缓存实例有效"); if (isReload) { - LogUtils.d(TAG, "getInstance():强制重载图片 | " + imagePath); + LogUtils.d(TAG, "【getInstance】强制重载图片 | 路径:" + imagePath); sCachedView.loadImage(imagePath); } else { - LogUtils.d(TAG, "getInstance():使用缓存实例,无需重载 | " + imagePath); + LogUtils.d(TAG, "【getInstance】使用缓存实例,无需重载 | 路径:" + imagePath); } return sCachedView; } - // 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存(核心修改:保留旧实例,仅更新引用) - LogUtils.d(TAG, "getInstance():路径未缓存,新建实例(保留旧实例) | " + imagePath); - MemoryCachedBackgroundView oldView = sCachedView; // 保留旧实例引用,防止被销毁 + // 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存(核心:保留旧实例,仅更新引用) + LogUtils.d(TAG, "【getInstance】路径未缓存,新建实例(保留旧实例) | 路径:" + imagePath); String oldPath = sCachedImagePath; sCachedView = new MemoryCachedBackgroundView(context); sCachedImagePath = imagePath; sCachedView.loadImage(imagePath); - LogUtils.d(TAG, "getInstance():已更新当前缓存实例,旧实例路径:" + oldPath + "(强制保持)"); + LogUtils.d(TAG, "【getInstance】已更新当前缓存实例,旧实例路径:" + oldPath + "(强制保持)"); return sCachedView; } @@ -91,28 +94,27 @@ public class MemoryCachedBackgroundView extends BackgroundView { * @return 最后加载路径对应的实例 */ public static MemoryCachedBackgroundView getLastInstance(Context context) { - LogUtils.d(TAG, "getLastInstance() 调用 | 当前实例总数:" + sInstanceCount); + LogUtils.d(TAG, "【getLastInstance】调用"); // 1. 从SP获取最后加载的路径(强制保持,不自动删除) String lastPath = getLastLoadImagePath(context); if (TextUtils.isEmpty(lastPath)) { - LogUtils.e(TAG, "getLastInstance():无最后加载路径,创建空实例"); + LogUtils.e(TAG, "【getLastInstance】无最后加载路径,创建空实例"); return new MemoryCachedBackgroundView(context); } // 2. 路径匹配当前缓存 → 直接返回 if (lastPath.equals(sCachedImagePath) && sCachedView != null) { - LogUtils.d(TAG, "getLastInstance():使用最后路径缓存实例 | " + lastPath); + LogUtils.d(TAG, "【getLastInstance】使用最后路径缓存实例 | 路径:" + lastPath); return sCachedView; } // 3. 路径不匹配 → 新建实例并更新缓存(保留旧实例) - LogUtils.d(TAG, "getLastInstance():最后路径未缓存,新建实例并加载(保留旧实例) | " + lastPath); - MemoryCachedBackgroundView oldView = sCachedView; + LogUtils.d(TAG, "【getLastInstance】最后路径未缓存,新建实例并加载(保留旧实例) | 路径:" + lastPath); String oldPath = sCachedImagePath; sCachedView = new MemoryCachedBackgroundView(context); sCachedImagePath = lastPath; sCachedView.loadImage(lastPath); - LogUtils.d(TAG, "getLastInstance():已更新最后路径实例,旧实例路径:" + oldPath + "(强制保持)"); + LogUtils.d(TAG, "【getLastInstance】已更新最后路径实例,旧实例路径:" + oldPath + "(强制保持)"); return sCachedView; } @@ -124,11 +126,12 @@ public class MemoryCachedBackgroundView extends BackgroundView { */ private static void saveLastLoadImagePath(Context context, String imagePath) { if (TextUtils.isEmpty(imagePath) || context == null) { + LogUtils.w(TAG, "【saveLastLoadImagePath】路径或上下文为空,跳过保存"); return; } SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); sp.edit().putString(KEY_LAST_LOAD_IMAGE_PATH, imagePath).apply(); - LogUtils.d(TAG, "saveLastLoadImagePath():已保存最后路径(强制保持) | " + imagePath); + LogUtils.d(TAG, "【saveLastLoadImagePath】已保存最后路径(强制保持) | 路径:" + imagePath); } /** @@ -138,11 +141,12 @@ public class MemoryCachedBackgroundView extends BackgroundView { */ public static String getLastLoadImagePath(Context context) { if (context == null) { + LogUtils.e(TAG, "【getLastLoadImagePath】上下文为空,返回null"); return null; } SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); String lastPath = sp.getString(KEY_LAST_LOAD_IMAGE_PATH, null); - LogUtils.d(TAG, "getLastLoadImagePath():获取最后路径(强制保持) | " + lastPath); + LogUtils.d(TAG, "【getLastLoadImagePath】获取最后路径(强制保持) | 路径:" + lastPath); return lastPath; } @@ -151,9 +155,8 @@ public class MemoryCachedBackgroundView extends BackgroundView { * 清除当前缓存实例和路径(强制缓存策略:仅日志,不实际清理) */ public static void clearCache() { - LogUtils.w(TAG, "clearCache() 调用(强制缓存策略:不实际清理缓存) | 当前缓存路径:" + sCachedImagePath); - // 核心修改:注释所有清理逻辑,仅保留日志 - LogUtils.d(TAG, "clearCache():强制缓存策略生效,未清除任何实例和路径"); + LogUtils.w(TAG, "【clearCache】调用(强制缓存策略:不实际清理缓存) | 当前缓存路径:" + sCachedImagePath); + LogUtils.d(TAG, "【clearCache】强制缓存策略生效,未清除任何实例和路径"); } /** @@ -161,22 +164,20 @@ public class MemoryCachedBackgroundView extends BackgroundView { * @param imagePath 图片路径 */ public static void removeCache(String imagePath) { - LogUtils.w(TAG, "removeCache() 调用(强制缓存策略:不实际清理缓存) | 图片路径:" + imagePath); + LogUtils.w(TAG, "【removeCache】调用(强制缓存策略:不实际清理缓存) | 图片路径:" + imagePath); if (TextUtils.isEmpty(imagePath)) { - LogUtils.e(TAG, "removeCache():图片路径为空,清除失败"); + LogUtils.e(TAG, "【removeCache】图片路径为空,清除失败"); return; } - // 核心修改:注释所有清理逻辑,仅保留日志 - LogUtils.d(TAG, "removeCache():强制缓存策略生效,未清除任何实例和路径"); + LogUtils.d(TAG, "【removeCache】强制缓存策略生效,未清除任何实例和路径"); } /** * 清除所有缓存(强制缓存策略:仅日志,不实际清理) */ public static void clearAllCache() { - LogUtils.w(TAG, "clearAllCache() 调用(强制缓存策略:不实际清理缓存)"); - // 核心修改:注释所有清理逻辑,仅保留日志 - LogUtils.d(TAG, "clearAllCache():强制缓存策略生效,未清除任何实例、路径和SP记录"); + LogUtils.w(TAG, "【clearAllCache】调用(强制缓存策略:不实际清理缓存)"); + LogUtils.d(TAG, "【clearAllCache】强制缓存策略生效,未清除任何实例、路径和SP记录"); } /** @@ -184,7 +185,9 @@ public class MemoryCachedBackgroundView extends BackgroundView { * @return 存在返回true,否则返回false */ public static boolean hasCache() { - return sCachedView != null && !TextUtils.isEmpty(sCachedImagePath); + boolean hasCache = sCachedView != null && !TextUtils.isEmpty(sCachedImagePath); + LogUtils.d(TAG, "【hasCache】缓存存在状态:" + hasCache); + return hasCache; } /** @@ -192,9 +195,8 @@ public class MemoryCachedBackgroundView extends BackgroundView { * @param context 上下文 */ public static void clearLastLoadImagePath(Context context) { - LogUtils.w(TAG, "clearLastLoadImagePath() 调用(强制缓存策略:不实际清理SP记录)"); - // 核心修改:注释所有清理逻辑,仅保留日志 - LogUtils.d(TAG, "clearLastLoadImagePath():强制缓存策略生效,未清除SP中最后路径记录"); + LogUtils.w(TAG, "【clearLastLoadImagePath】调用(强制缓存策略:不实际清理SP记录)"); + LogUtils.d(TAG, "【clearLastLoadImagePath】强制缓存策略生效,未清除SP中最后路径记录"); } // ====================================== 辅助方法:从缓存获取上下文 ====================================== @@ -203,13 +205,15 @@ public class MemoryCachedBackgroundView extends BackgroundView { * @return 上下文实例,无则返回null */ private static Context getContextFromCache() { - return sCachedView != null ? sCachedView.getContext() : null; + Context context = sCachedView != null ? sCachedView.getContext() : null; + LogUtils.d(TAG, "【getContextFromCache】从缓存获取上下文:" + context); + return context; } // ====================================== 重写父类方法:增强日志+SP持久化(强制保持版) ====================================== @Override public void loadImage(String imagePath) { - LogUtils.d(TAG, "loadImage() 重载方法调用 | 图片路径:" + imagePath); + LogUtils.d(TAG, "【loadImage】调用 | 图片路径:" + imagePath); super.loadImage(imagePath); // 保存最后加载路径到SP(强制保持,不自动删除) saveLastLoadImagePath(getContext(), imagePath); @@ -217,13 +221,13 @@ public class MemoryCachedBackgroundView extends BackgroundView { @Override public void loadByBackgroundBean(BackgroundBean bean) { - LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString())); + LogUtils.d(TAG, "【loadByBackgroundBean】调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString())); super.loadByBackgroundBean(bean); } @Override public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) { - LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh); + LogUtils.d(TAG, "【loadByBackgroundBean】调用 | BackgroundBean:" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh); super.loadByBackgroundBean(bean, isRefresh); } @@ -233,7 +237,7 @@ public class MemoryCachedBackgroundView extends BackgroundView { * @return 实例总数 */ public static int getInstanceCount() { - LogUtils.d(TAG, "getInstanceCount() 调用 | 当前实例总数:" + sInstanceCount); + LogUtils.d(TAG, "【getInstanceCount】调用 | 当前实例总数:" + sInstanceCount); return sInstanceCount; } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/VerticalSeekBar.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/VerticalSeekBar.java index d5343f3..fcedfcb 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/VerticalSeekBar.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/VerticalSeekBar.java @@ -8,10 +8,10 @@ import android.widget.SeekBar; import cc.winboll.studio.libappbase.LogUtils; /** + * 垂直进度条控件,适配 API30,支持逆时针旋转(0在下,100在上) + * 修复滑块同步+弹窗触发bug,新增实时进度变化监听接口,支持拖动时实时回调进度 * @Author ZhanGSKen&豆包大模型 * @Date 2025/12/17 14:11 - * @Describe 垂直进度条控件,适配 API30,支持逆时针旋转(0在下,100在上),修复滑块同步+弹窗触发bug - * 新增:实时进度变化监听接口,支持拖动时实时回调进度 */ public class VerticalSeekBar extends SeekBar { // ======================== 静态常量 ========================= @@ -76,26 +76,26 @@ public class VerticalSeekBar extends SeekBar { public VerticalSeekBar(Context context) { super(context); initView(); - LogUtils.d(TAG, "VerticalSeekBar(Context) 初始化"); + LogUtils.d(TAG, "【构造器1】VerticalSeekBar 初始化完成"); } public VerticalSeekBar(Context context, AttributeSet attrs) { super(context, attrs); initView(); - LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet) 初始化"); + LogUtils.d(TAG, "【构造器2】VerticalSeekBar 初始化完成"); } public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); - LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet, int) 初始化"); + LogUtils.d(TAG, "【构造器3】VerticalSeekBar 初始化完成"); } // ======================== 初始化方法 ========================= private void initView() { // 移除水平默认阴影,优化垂直显示效果,减少 API30 不必要的绘制开销 setBackgroundDrawable(null); - LogUtils.d(TAG, "initView: 移除默认背景阴影,完成视图初始化"); + LogUtils.d(TAG, "【initView】移除默认背景阴影,完成视图初始化"); } // ======================== 对外设置方法(监听接口绑定)======================== @@ -105,7 +105,7 @@ public class VerticalSeekBar extends SeekBar { */ public void setOnVerticalSeekBarTouchListener(OnVerticalSeekBarTouchListener listener) { this.mTouchListener = listener; - LogUtils.d(TAG, "setOnVerticalSeekBarTouchListener: 触摸监听器绑定完成"); + LogUtils.d(TAG, "【setOnVerticalSeekBarTouchListener】触摸监听器绑定完成"); } /** @@ -114,7 +114,7 @@ public class VerticalSeekBar extends SeekBar { */ public void setOnVerticalSeekBarChangeListener(OnVerticalSeekBarChangeListener listener) { this.mProgressChangeListener = listener; - LogUtils.d(TAG, "setOnVerticalSeekBarChangeListener: 实时进度监听器绑定完成"); + LogUtils.d(TAG, "【setOnVerticalSeekBarChangeListener】实时进度监听器绑定完成"); } // ======================== 重写系统方法(测量/布局/绘制)======================== @@ -122,13 +122,13 @@ public class VerticalSeekBar extends SeekBar { protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(heightMeasureSpec, widthMeasureSpec); setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); - LogUtils.v(TAG, "onMeasure: 垂直测量完成,宽=" + getMeasuredHeight() + ", 高=" + getMeasuredWidth()); + LogUtils.v(TAG, "【onMeasure】垂直测量完成,宽=" + getMeasuredHeight() + ",高=" + getMeasuredWidth()); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(h, w, oldh, oldw); - LogUtils.v(TAG, "onSizeChanged: 尺寸变化,新宽=" + h + ", 新高=" + w); + LogUtils.v(TAG, "【onSizeChanged】尺寸变化,新宽=" + h + ",新高=" + w); } @Override @@ -137,7 +137,7 @@ public class VerticalSeekBar extends SeekBar { canvas.rotate(-90); canvas.translate(-getHeight(), 0); super.onDraw(canvas); - LogUtils.v(TAG, "onDraw: 完成垂直绘制,旋转角度=-90°"); + LogUtils.v(TAG, "【onDraw】完成垂直绘制,旋转角度=-90°"); } // ======================== 重写进度设置方法(修复滑块同步+新增实时回调)======================== @@ -151,10 +151,11 @@ public class VerticalSeekBar extends SeekBar { // 强制触发尺寸变化,同步刷新滑块位置(核心bug修复逻辑) onSizeChanged(getWidth(), getHeight(), 0, 0); mProgress = progress; - LogUtils.d(TAG, "setProgress: 进度设置为" + progress + ",滑块同步刷新"); + LogUtils.d(TAG, "【setProgress】进度设置为" + progress + ",滑块同步刷新"); // 触发实时进度监听(外部调用 setProgress 时 fromUser 为 false) if (mProgressChangeListener != null) { mProgressChangeListener.onProgressChanged(this, progress, false); + LogUtils.v(TAG, "【setProgress】触发实时进度回调,fromUser=false"); } } @@ -165,20 +166,22 @@ public class VerticalSeekBar extends SeekBar { super.onTouchEvent(event); boolean handled = true; // 强制消费事件,避免事件被拦截导致回调丢失 boolean fromUser = true; // 标记是否是用户触摸导致的进度变化 + int action = event.getAction(); - switch (event.getAction()) { + switch (action) { case MotionEvent.ACTION_DOWN: - LogUtils.d(TAG, "onTouchEvent: 触摸按下,Y坐标=" + event.getY()); + LogUtils.d(TAG, "【onTouchEvent】触摸按下,Y坐标=" + event.getY()); // 触发实时进度监听:开始触摸 if (mProgressChangeListener != null) { mProgressChangeListener.onStartTrackingTouch(this); + LogUtils.v(TAG, "【onTouchEvent】触发开始触摸回调"); } break; case MotionEvent.ACTION_MOVE: calculateProgress(event.getY()); setProgress(mProgress); - LogUtils.v(TAG, "onTouchEvent: 触摸滑动,进度更新为" + mProgress); + LogUtils.v(TAG, "【onTouchEvent】触摸滑动,进度更新为" + mProgress); // 触发实时进度监听:进度变化 if (mProgressChangeListener != null) { mProgressChangeListener.onProgressChanged(this, mProgress, fromUser); @@ -188,27 +191,31 @@ public class VerticalSeekBar extends SeekBar { case MotionEvent.ACTION_UP: calculateProgress(event.getY()); setProgress(mProgress); - LogUtils.d(TAG, "onTouchEvent: 触摸抬起,进度=" + mProgress + ",触发弹窗回调"); - // 触发实时进度监听:停止触摸 + LogUtils.d(TAG, "【onTouchEvent】触摸抬起,进度=" + mProgress + ",触发弹窗回调"); + // 触发实时进度监听:进度变化+停止触摸 if (mProgressChangeListener != null) { mProgressChangeListener.onProgressChanged(this, mProgress, fromUser); mProgressChangeListener.onStopTrackingTouch(this); + LogUtils.v(TAG, "【onTouchEvent】触发停止触摸回调"); } // 核心:调用原有触摸接口,通知外部触发配置变更对话框 if (mTouchListener != null) { mTouchListener.onTouchUp(this, mProgress); + LogUtils.v(TAG, "【onTouchEvent】触发触摸抬起回调"); } break; case MotionEvent.ACTION_CANCEL: - LogUtils.d(TAG, "onTouchEvent: 触摸取消,当前进度=" + getProgress()); + int currentProgress = getProgress(); + LogUtils.d(TAG, "【onTouchEvent】触摸取消,当前进度=" + currentProgress); // 触发实时进度监听:停止触摸 if (mProgressChangeListener != null) { mProgressChangeListener.onStopTrackingTouch(this); } // 可选:触摸取消时回调,外部可做进度回滚处理 if (mTouchListener != null) { - mTouchListener.onTouchCancel(this, getProgress()); + mTouchListener.onTouchCancel(this, currentProgress); + LogUtils.v(TAG, "【onTouchEvent】触发触摸取消回调"); } break; } @@ -225,7 +232,7 @@ public class VerticalSeekBar extends SeekBar { mProgress = getMax() - (int) (getMax() * touchY / getHeight()); // 校准进度范围,防止超出 0~100(兼容 API30 进度边界校验) mProgress = Math.max(0, Math.min(mProgress, getMax())); - LogUtils.v(TAG, "calculateProgress: 触摸Y=" + touchY + ",计算进度=" + mProgress); + LogUtils.v(TAG, "【calculateProgress】触摸Y=" + touchY + ",计算进度=" + mProgress + ",校准后=" + mProgress); } } diff --git a/powerbell/src/main/res/values-zh/strings.xml b/powerbell/src/main/res/values-zh/strings.xml index 6eb6a70..6c1132f 100644 --- a/powerbell/src/main/res/values-zh/strings.xml +++ b/powerbell/src/main/res/values-zh/strings.xml @@ -22,4 +22,5 @@ 背景像素拾取 关于应用 >>>向右滑动100%可以清理电量记录。>>> + 无电池记录数据 diff --git a/powerbell/src/main/res/values/strings.xml b/powerbell/src/main/res/values/strings.xml index a81cfdd..5490a15 100644 --- a/powerbell/src/main/res/values/strings.xml +++ b/powerbell/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Pixel Picker About The APP >>>Seek 100% Right Is Clean Record.>>> + No Battery Record 权限申请