Compare commits

..

65 Commits

Author SHA1 Message Date
eab26dbba2 添加应用启动时的权限检查 2025-12-04 18:31:36 +08:00
5d4a5f25ad <powerbell>APK 15.12.0 release Publish. 2025-12-04 17:33:40 +08:00
858b874ea1 根据源码整体改动情况决定,版本号升级一个阶段,对应用发布进行阶段性的分级。 2025-12-04 17:32:00 +08:00
2e41aae853 <powerbell>APK 15.11.13 release Publish. 2025-12-04 16:59:16 +08:00
56a9a2f476 优化广告排版 2025-12-04 16:56:07 +08:00
2a2c006264 图片拾取,图片裁剪与图片设置流程测试完成。 2025-12-04 16:33:24 +08:00
df51b415fb 引入第三方图片裁剪类库 2025-12-04 16:23:17 +08:00
19e6e276bd 20251204_134218_045 2025-12-04 13:42:30 +08:00
7f3c91fb1d 解耦类型功能 2025-12-04 06:59:54 +08:00
4434221827 两个类源码整合完成 2025-12-04 06:38:14 +08:00
75415956eb 设置窗口源码合并完成 2025-12-04 06:28:50 +08:00
c930308425 20251204_051841_861 2025-12-04 05:18:46 +08:00
8a16728609 20251204_025812_384 2025-12-04 02:58:16 +08:00
59992542c4 20251204_002634_939 2025-12-04 00:26:47 +08:00
fe5dd9e1ab 20251203_211951_581BackgroundView 。布局文件调整。。。 2025-12-03 21:20:26 +08:00
0d99057880 设置自由剪裁图片功能 2025-12-03 20:57:27 +08:00
ca66120e55 背景布局调试... 2025-12-03 20:19:18 +08:00
ca1850fe8a 修改设置图片后背景图片数据丢失问题。 2025-12-03 19:31:58 +08:00
4a31b9eef0 基本完成图片设置流程。繁琐步骤未测试。。。 2025-12-03 19:04:59 +08:00
c6a6826102 源码整理 2025-12-03 18:02:01 +08:00
16c44e5e0e 图片选择与剪裁阶段调试完成 2025-12-03 16:29:52 +08:00
8fb7147333 提高应用调试性能 2025-12-02 14:09:24 +08:00
e190d3ff39 BackgroundView 类重构 2025-12-02 13:58:04 +08:00
a997fb01c8 源码整理 2025-12-02 13:09:40 +08:00
637d4577df 文件管理模块重构。 2025-12-02 06:01:38 +08:00
33f1b430a4 20251202_025211_572提高应用调试能力 2025-12-02 02:52:45 +08:00
f6a00fac36 20251202_022447_396 2025-12-02 02:24:50 +08:00
5d3d46f2fe 图片背景放置方式改进中。。。 2025-12-02 00:36:29 +08:00
ed660aa4ef 20251201_174639_788 2025-12-01 17:46:43 +08:00
ee4b0ca6d9 重构权限申请模块,图片选择剪裁部分测试通过。 2025-12-01 17:36:03 +08:00
6538ebafef 图片剪裁与预览测试完成 2025-12-01 15:59:49 +08:00
1cadc4ed93 20251201_151821_718 2025-12-01 15:18:26 +08:00
04b8906a96 20251201_101834_179 2025-12-01 10:18:45 +08:00
e14744b2ac 图片剪裁调试完成 2025-12-01 09:57:49 +08:00
4e7b7daa42 20251201_085930_533 2025-12-01 08:59:33 +08:00
f7ef8f6b19 文件管理类规范划分 2025-12-01 08:20:45 +08:00
6951f642a1 20251201_072450_660 2025-12-01 07:24:55 +08:00
a6b25eaf2b 20251201_061446_148 2025-12-01 06:14:56 +08:00
80363c6b4c 20251201_042831_062 2025-12-01 04:29:06 +08:00
66e3e602e5 20251201_040959_262 2025-12-01 04:10:10 +08:00
9b010df881 20251201_023218_404 2025-12-01 02:32:28 +08:00
3c3bcc4ee4 调试到选择图片按钮到剪裁图片阶段 2025-12-01 02:07:29 +08:00
21c712f7b3 20251201_011410_083 2025-12-01 01:14:14 +08:00
e408b5cbde 20251201_004105_116 2025-12-01 00:41:10 +08:00
5a9469317d <powerbell>APK 15.11.12 release Publish. 2025-11-30 18:48:20 +08:00
0d5f7f40cd 编译参数修复 2025-11-30 17:23:39 +08:00
64e5bc753a 提高应用崩溃信息UI友好性。 2025-11-30 17:21:58 +08:00
13b6af6921 <powerbell>APK 15.11.11 release Publish. 2025-11-30 03:50:23 +08:00
e2703495ae 更新基础类库以改进应用崩溃处理方法 2025-11-30 03:47:36 +08:00
c9bc5c88d8 20251129_170510_937 2025-11-29 17:05:15 +08:00
34356b8116 20251129_163631_757 2025-11-29 16:36:37 +08:00
de189c3fb0 20251129_161627_420整体函数重构完成,待调试。。。 2025-11-29 16:17:12 +08:00
c4b2ecaecb 调试到下载图片,未调试图片存储管理。 2025-11-29 11:25:41 +08:00
1b53594086 更正基础类库,方便调试,继续调试。。。 2025-11-29 02:51:29 +08:00
6a2d011ceb 20251127_214905_683 Debugging ... 2025-11-27 21:49:29 +08:00
5aa54091e5 20251127_212052_680正在调试图片文件存储问题。 2025-11-27 21:21:25 +08:00
bc8a63867c <powerbell>APK 15.11.10 release Publish. 2025-11-27 19:24:29 +08:00
2a10cb493d 更新AES类库,更新广告控制UI界面。 2025-11-27 19:22:05 +08:00
ea7e2f8366 <powerbell>APK 15.11.9 release Publish. 2025-11-27 14:46:03 +08:00
d073a86b9b 添加应用设置与米盟广告SDK设置控件。 2025-11-27 14:43:53 +08:00
e76427eac8 修复背景图片右边有缝隙,以及像素背景设置的问题。 2025-11-27 14:25:37 +08:00
af53216af3 添加中文资源字符串 2025-11-27 13:34:55 +08:00
3ae56bb202 升级基础类库 2025-11-27 13:27:49 +08:00
09f1974c8e 更新类标签 2025-11-27 08:58:41 +08:00
721c93c4b2 更新应用快捷菜单提示 2025-11-27 08:51:06 +08:00
233 changed files with 4895 additions and 12214 deletions

View File

@@ -3,11 +3,8 @@
########
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/APPBase> ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/appbase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
## WinBoLL 提问
同样是 /sdcard 目录,在开发 Android 应用时,

View File

@@ -1,7 +1,7 @@
# AES
#### 介绍
WinBoLL 安卓可视化元素类库测试应用。
安卓视图元素类库
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
@@ -32,4 +32,4 @@ WinBoLL 安卓可视化元素类库测试应用。
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档
#### 参考文档

View File

@@ -38,10 +38,12 @@ android {
versionName = genVersionName("${versionName}")
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Nov 26 15:54:26 GMT 2025
stageCount=7
#Wed Nov 19 09:04:33 HKT 2025
stageCount=5
libraryProject=libaes
baseVersion=15.11
publishVersion=15.11.6
buildCount=32
baseBetaVersion=15.11.7
publishVersion=15.11.4
buildCount=0
baseBetaVersion=15.11.5

132
aes/proguard-rules.pro vendored
View File

@@ -9,129 +9,9 @@
# Add any project specific keep options here:
# ============================== 基础通用规则 ==============================
# 保留系统组件
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
# 保留 WinBoLL 核心包及子类(合并简化规则)
-keep class cc.winboll.studio.** { *; }
-keepclassmembers class cc.winboll.studio.** { *; }
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
-keepclassmembers class * {
public static final java.lang.String TAG;
}
# 保留序列化类避免Parcelable/Gson解析异常
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留 R 文件避免资源ID混淆
-keepclassmembers class **.R$* {
public static <fields>;
}
# 保留 native 方法避免JNI调用失败
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留注解和泛型(避免反射/序列化异常)
-keepattributes *Annotation*
-keepattributes Signature
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
-dontwarn java.lang.invoke.*
-dontwarn android.support.v8.renderscript.*
-dontwarn java.util.function.**
# ============================== 第三方框架专项规则 ==============================
# OkHttp 4.4.1米盟广告请求依赖完善Lambda兼容
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class okhttp3.internal.** { *; }
-keep class okio.** { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn okio.**
# Glide 4.9.0(米盟广告图片加载依赖)
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
**[] $VALUES;
public *;
}
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
<init>();
}
-dontwarn com.bumptech.glide.**
# Gson 2.8.5(米盟广告数据序列化依赖)
-keep class com.google.gson.** { *; }
-keep interface com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
-keep class com.miui.zeus.** { *; }
-keep interface com.miui.zeus.** { *; }
# 保留米盟日志字段(便于广告加载失败排查)
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
public static final java.lang.String TAG;
}
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
-keep class androidx.recyclerview.** { *; }
-keep interface androidx.recyclerview.** { *; }
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
public *;
}
# 其他第三方框架(按引入依赖保留,无则可删除)
# XXPermissions 18.63
-keep class com.hjq.permissions.** { *; }
-keep interface com.hjq.permissions.** { *; }
# ZXing 二维码(核心解析组件)
-keep class com.google.zxing.** { *; }
-keep class com.journeyapps.zxing.** { *; }
# Jsoup HTML解析
-keep class org.jsoup.** { *; }
# Pinyin4j 拼音搜索
-keep class net.sourceforge.pinyin4j.** { *; }
# JSch SSH组件
-keep class com.jcraft.jsch.** { *; }
# AndroidX 基础组件
-keep class androidx.appcompat.** { *; }
-keep interface androidx.appcompat.** { *; }
# ============================== 优化与调试配置 ==============================
# 优化级别(平衡混淆效果与性能)
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 调试辅助(保留行号便于崩溃定位)
-verbose
-dontpreverify
-dontusemixedcaseclassnames
-keepattributes SourceFile,LineNumberTable
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -35,8 +35,6 @@
<activity android:name=".TestActivityManagerActivity"/>
<activity android:name=".SettingsActivity"/>
</application>
</manifest>

View File

@@ -83,11 +83,11 @@ public class AboutActivity extends WinBoLLActivity implements IWinBoLLActivity {
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(szBranchName);
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=AES");
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=3&extra=page%3D1");
appInfo.setAppAPKName("AES");
appInfo.setAppAPKFolderName("AES");
//appInfo.setIsAddDebugTools(false);
//appInfo.setIsAddDebugTools(BuildConfig.DEBUG);
appInfo.setIsAddDebugTools(BuildConfig.DEBUG);
return new AboutView(mContext, appInfo);
}
}

View File

@@ -90,7 +90,7 @@ public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActi
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_main, menu);
getMenuInflater().inflate(R.menu.toolbar_library, menu);
if(App.isDebugging()) {
getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_studio_debug, menu);
}
@@ -185,10 +185,8 @@ public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActi
} else if (nItemId == R.id.item_drawerfragmentactivity) {
Intent intent = new Intent(this, TestDrawerFragmentActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
} else if (nItemId == R.id.item_about) {
}
else if (nItemId == R.id.item_about) {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
return true;

View File

@@ -1,38 +0,0 @@
package cc.winboll.studio.aes;
import android.app.Activity;
import android.os.Bundle;
import cc.winboll.studio.libaes.enums.ADsMode;
import cc.winboll.studio.libaes.views.ADsControlView;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/26 18:01
* @Describe SettingsActivity
*/
public class SettingsActivity extends Activity {
public static final String TAG = "SettingsActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
// @Override
// public void onModeSelected(ADsMode selectedMode) {
// if (selectedMode == ADsMode.STANDALONE) {
// // 处理单机模式逻辑(如释放米盟资源)
// ToastUtils.show("STANDALONE");
// } else if (selectedMode == ADsMode.MIMO_SDK) {
// // 处理米盟SDK模式逻辑如初始化SDK
// ToastUtils.show("MIMO_SDK");
// }
// }
// });
}
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -32,7 +32,4 @@
<item
android:id="@+id/item_drawerfragmentactivity"
android:title="Test DrawerFragmentActivity"/>
<item
android:id="@+id/item_settings"
android:title="Settings"/>
</menu>

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Nov 30 11:43:55 HKT 2025
stageCount=6
#Fri Nov 21 11:41:04 HKT 2025
stageCount=2
libraryProject=libappbase
baseVersion=15.11
publishVersion=15.11.5
publishVersion=15.11.1
buildCount=0
baseBetaVersion=15.11.6
baseBetaVersion=15.11.2

View File

@@ -2,7 +2,7 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.appbase">
<application
android:name=".App"
android:icon="@drawable/ic_winboll"
@@ -11,7 +11,7 @@
android:resizeableActivity="true"
android:process=":App">
<activity
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
@@ -29,15 +29,13 @@
</intent-filter>
</activity>
<activity android:name=".GlobalApplication$CrashActivity"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
</application>
</manifest>
</manifest>

View File

@@ -2,7 +2,6 @@ package cc.winboll.studio.appbase;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.BuildConfig;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -22,8 +21,6 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置)
//setIsDebugging(false);
setIsDebugging(BuildConfig.DEBUG);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
}

View File

@@ -1,35 +0,0 @@
# APPUtils
#### 介绍
应用开发工具套件类
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 git pull && bash .winboll/bashPublishAPKAddTag.sh apputils
阶段版类库发布命令 git pull &&bash .winboll/bashPublishLIBAddTag.sh libapputils
#### 使用说明
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
#### 参考文档

View File

@@ -29,7 +29,7 @@ android {
// versionName 更新后需要手动设置
// 项目模块目录的 build.gradle 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.10"
versionName "15.8"
if(true) {
versionName = genVersionName("${versionName}")
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Sep 29 01:16:05 HKT 2025
stageCount=3
#Mon Sep 01 07:56:33 HKT 2025
stageCount=7
libraryProject=libapputils
baseVersion=15.10
publishVersion=15.10.2
baseVersion=15.8
publishVersion=15.8.6
buildCount=0
baseBetaVersion=15.10.3
baseBetaVersion=15.8.7

View File

@@ -2,20 +2,14 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.apputils">
<!-- 读取外部存储权限Android 10 以下) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".App"
android:allowBackup="true"
android:icon="@drawable/ic_winboll"
android:label="@string/app_name"
android:theme="@style/MyUtilsTheme"
android:supportsRtl="true"
android:resizeableActivity="true"
android:screenOrientation="unspecified"
android:requestLegacyExternalStorage="true">
android:supportsRtl="true">
<activity
android:name=".MainActivity"
@@ -41,8 +35,6 @@
<activity android:name=".QRCodeDecodeActivity"/>
<activity android:name=".QRGeneratorActivity"/>
</application>
</manifest>
</manifest>

View File

@@ -15,11 +15,12 @@ import android.view.MenuItem;
import android.widget.Toolbar;
import cc.winboll.studio.apputils.R;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
import cc.winboll.studio.libapputils.views.SimpleWebView;
import java.io.IOException;
import java.io.InputStream;
public class AssetsHtmlActivity extends Activity {
public class AssetsHtmlActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "AssetsHtmlActivity";
@@ -31,6 +32,16 @@ public class AssetsHtmlActivity extends Activity {
// Assets 文件夹里的 Html 文件的名称
String mszHtmlFileName;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {

View File

@@ -15,10 +15,9 @@ import android.widget.Toolbar;
import cc.winboll.studio.apputils.R;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import java.util.List;
import java.util.Set;
import cc.winboll.studio.libappbase.LogActivity;
final public class MainActivity extends Activity {
@@ -27,21 +26,21 @@ final public class MainActivity extends Activity {
public static final int REQUEST_QRCODEDECODE_ACTIVITY = 0;
Toolbar mToolbar;
//LogView mLogView;
LogView mLogView;
//
// @Override
// public Activity getActivity() {
// return this;
// }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// mLogView = findViewById(R.id.logview);
// mLogView.start();
mLogView = findViewById(R.id.logview);
mLogView.start();
// 初始化工具栏
mToolbar = findViewById(R.id.toolbar);
@@ -146,21 +145,13 @@ final public class MainActivity extends Activity {
}
public void onTestLogActivity(View view) {
/* 分屏代码有效
// 1. 创建启动 SecondActivity 的 Intent
Intent splitIntent = new Intent(MainActivity.this, LogActivity.class);
// Intent intent = new Intent(this, LogActivity.class);
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
// intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
// startActivity(intent);
// 2. 添加分屏启动必需的两个标志API 30 兼容)
// FLAG_ACTIVITY_LAUNCH_ADJACENT相邻分屏显示
// FLAG_ACTIVITY_NEW_TASK分屏需要新任务栈
splitIntent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
| Intent.FLAG_ACTIVITY_NEW_TASK);
// 3. 启动分屏活动(若设备不支持分屏,会默认全屏启动)
startActivity(splitIntent);
*/
LogActivity.startLogActivity(this);
//WinBoLLActivityManager.getInstance().printAvtivityListInfo();
//WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, LogActivity.class);
}
//
@@ -226,9 +217,10 @@ final public class MainActivity extends Activity {
if (item.getItemId() == R.id.item_exit) {
//exit();
return true;
} else if (item.getItemId() == R.id.item_testqrgeneratoractivity) {
Intent intent = new Intent(this, QRGeneratorActivity.class);
startActivity(intent);
// } else if (item.getItemId() == R.id.item_teststringtoqrcodeview) {
// Intent intent = new Intent(this, TestStringToQRCodeViewActivity.class);
// startActivityForResult(intent, REQUEST_QRCODEDECODE_ACTIVITY);
// //WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, TestStringToQrCodeViewActivity.class);
} else if (item.getItemId() == R.id.item_testqrcodedecodeactivity) {
Intent intent = new Intent(this, QRCodeDecodeActivity.class);
startActivityForResult(intent, REQUEST_QRCODEDECODE_ACTIVITY);
@@ -276,7 +268,7 @@ final public class MainActivity extends Activity {
// }
}
public void onTestAssetsHtmlActivity(View view) {
Intent intent = new Intent(this, AssetsHtmlActivity.class);
intent.putExtra(AssetsHtmlActivity.EXTRA_HTMLFILENAME, "javascript_test.html");
@@ -289,7 +281,7 @@ final public class MainActivity extends Activity {
@Override
protected void onResume() {
super.onResume();
//mLogView.start();
mLogView.start();
}
/*@Override

View File

@@ -1,323 +1,89 @@
package cc.winboll.studio.apputils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/01/18 10:32:21
* @Describe 二维码解码窗口
* @Describe 二维码扫码解码窗口
*/
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toolbar;
import cc.winboll.studio.apputils.R;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.ResultPoint;
import com.google.zxing.common.HybridBinarizer;
import com.journeyapps.barcodescanner.BarcodeCallback;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
import com.journeyapps.barcodescanner.DefaultDecoderFactory;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
public class QRCodeDecodeActivity extends Activity {
public static final String TAG = "QRCodeDecodeActivity";
public static final String EXTRA_RESULT = "EXTRA_RESULT";
private static final int REQUEST_CAMERA_PERMISSION = 1;
private static final int REQUEST_PICK_IMAGE = 2;
private static final int REQUEST_READ_STORAGE_PERMISSION = 3;
// 图片压缩阈值超过1000px时压缩避免内存溢出和解码效率低
private static final int MAX_BITMAP_SIZE = 1000;
private TextView resultTextView;
private DecoratedBarcodeView barcodeView;
private Button btnDecodeFromAlbum;
TextView resultTextView;
DecoratedBarcodeView barcodeView;
// @Override
// public Activity getActivity() {
// return this;
// }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qrcodedecode);
initToolbar();
initViews();
checkCameraPermission();
setAlbumDecodeClickListener();
}
private void initToolbar() {
Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
if (mToolbar != null) {
setActionBar(mToolbar);
if (getActionBar() != null) {
getActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}
}
// 初始化工具栏
// Toolbar mToolbar = findViewById(R.id.toolbar);
// setActionBar(mToolbar);
private void initViews() {
resultTextView = (TextView) findViewById(R.id.activityqrcodedecodeTextView1);
barcodeView = (DecoratedBarcodeView) findViewById(R.id.activityqrcodedecodeDecoratedBarcodeView1);
btnDecodeFromAlbum = (Button) findViewById(R.id.btn_decode_from_album);
// 初始化扫码解码器支持所有常见码制避免仅支持QR_CODE的局限
List<BarcodeFormat> formats = new ArrayList<BarcodeFormat>();
formats.add(BarcodeFormat.QR_CODE);
formats.add(BarcodeFormat.CODE_128);
formats.add(BarcodeFormat.EAN_13);
barcodeView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(formats));
}
private void setAlbumDecodeClickListener() {
btnDecodeFromAlbum.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_READ_STORAGE_PERMISSION);
} else {
openAlbum();
}
}
});
}
private void openAlbum() {
Intent pickImageIntent = new Intent(Intent.ACTION_PICK);
pickImageIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(pickImageIntent, REQUEST_PICK_IMAGE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_PICK_IMAGE && resultCode == RESULT_OK && data != null) {
Uri selectedImageUri = data.getData();
if (selectedImageUri != null) {
try {
// 1. 读取图片并压缩(关键优化:避免大图片解码失败)
InputStream imageStream = getContentResolver().openInputStream(selectedImageUri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 先获取图片尺寸,不加载像素
BitmapFactory.decodeStream(imageStream, null, options);
imageStream.close(); // 关闭流,重新读取
// 计算压缩比例超过MAX_BITMAP_SIZE时按比例压缩
options.inSampleSize = calculateInSampleSize(options, MAX_BITMAP_SIZE, MAX_BITMAP_SIZE);
options.inJustDecodeBounds = false; // 开始加载压缩后的像素
imageStream = getContentResolver().openInputStream(selectedImageUri);
Bitmap originalBitmap = BitmapFactory.decodeStream(imageStream, null, options);
imageStream.close();
if (originalBitmap == null) {
showToast("图片损坏,无法解析");
return;
}
// 2. 图片预处理:转为灰度图+提高对比度(解决模糊/低对比度图片识别问题)
Bitmap processedBitmap = processBitmap(originalBitmap);
// 3. 解码预处理后的图片
String decodeResult = decodeQrFromBitmap(processedBitmap);
// 4. 结果处理
if (decodeResult != null && !decodeResult.isEmpty()) {
resultTextView.setText("图片解码结果:" + decodeResult);
showDecodeResultDialog(decodeResult);
returnResultToPreviousPage(decodeResult);
} else {
// 尝试直接解码原图(防止预处理过度导致识别失败)
String originalResult = decodeQrFromBitmap(originalBitmap);
if (originalResult != null && !originalResult.isEmpty()) {
resultTextView.setText("图片解码结果:" + originalResult);
showDecodeResultDialog(originalResult);
returnResultToPreviousPage(originalResult);
} else {
new AlertDialog.Builder(this)
.setTitle("解码失败")
.setMessage("图片中未识别到二维码/条码,建议选择清晰、完整的图片")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}
}
// 回收Bitmap避免内存泄漏
if (!originalBitmap.isRecycled()) originalBitmap.recycle();
if (!processedBitmap.isRecycled() && processedBitmap != originalBitmap) {
processedBitmap.recycle();
}
} catch (Exception e) {
e.printStackTrace();
showToast("图片处理失败:" + e.getMessage());
}
} else {
showToast("未选择图片");
}
}
}
/**
* 核心优化1计算图片压缩比例
*/
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 找到最接近reqWidth/reqHeight的压缩比例2的倍数保证图片质量
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
/**
* 核心优化2图片预处理灰度化+提高对比度)
* 解决模糊、低亮度、低对比度图片识别率低的问题
*/
private Bitmap processBitmap(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 创建灰度图
Bitmap grayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(grayBitmap);
Paint paint = new Paint();
// 1. 灰度化矩阵
ColorMatrix grayMatrix = new ColorMatrix();
grayMatrix.setSaturation(0); // 饱和度设为0转为灰度
// 2. 提高对比度矩阵alpha=1.5,亮度=0可根据需求调整
ColorMatrix contrastMatrix = new ColorMatrix();
contrastMatrix.set(new float[]{
1.5f, 0, 0, 0, 0, // 红通道对比度
0, 1.5f, 0, 0, 0, // 绿通道对比度
0, 0, 1.5f, 0, 0, // 蓝通道对比度
0, 0, 0, 1f, 0 // alpha通道不变
});
// 合并灰度+对比度矩阵
ColorMatrix combinedMatrix = new ColorMatrix();
combinedMatrix.postConcat(grayMatrix);
combinedMatrix.postConcat(contrastMatrix);
paint.setColorFilter(new ColorMatrixColorFilter(combinedMatrix));
canvas.drawBitmap(bitmap, 0, 0, paint);
return grayBitmap;
}
/**
* 核心优化3修复解码参数支持更多场景
*/
private String decodeQrFromBitmap(Bitmap bitmap) {
if (bitmap == null) return null;
try {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
// 修复1使用RGBLuminanceSource避免YUV格式导致的颜色偏差
LuminanceSource source = new RGBLuminanceSource(width, height, pixels);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
// 修复2完善解码参数解决模糊、变形二维码识别问题
Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>();
// 支持所有常见码制不仅限于QR_CODE
List<BarcodeFormat> formats = new ArrayList<BarcodeFormat>();
formats.add(BarcodeFormat.QR_CODE);
formats.add(BarcodeFormat.CODE_128);
formats.add(BarcodeFormat.EAN_8);
formats.add(BarcodeFormat.EAN_13);
hints.put(DecodeHintType.POSSIBLE_FORMATS, formats);
// 字符编码:支持中文等多语言
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
// 容错模式:允许二维码有一定损坏(关键!解决轻微变形/污染的二维码)
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
// 不使用纯条码模式兼容带logo的二维码
hints.put(DecodeHintType.PURE_BARCODE, Boolean.FALSE);
MultiFormatReader reader = new MultiFormatReader();
reader.setHints(hints);
Result result = reader.decode(binaryBitmap);
return result.getText();
} catch (NotFoundException e) {
// 正常未识别到,不打印异常(避免日志冗余)
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// ==================== 原有逻辑(不变) ====================
private void checkCameraPermission() {
if (checkSelfPermission(android.Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{android.Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);
} else {
startScanning();
}
//resultTextView = findViewById(R.id.activityqrcodedecodeTextView1);
barcodeView = findViewById(R.id.activityqrcodedecodeDecoratedBarcodeView1);
// 请求相机权限
// if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA)
// != PackageManager.PERMISSION_GRANTED) {
// ActivityCompat.requestPermissions(this,
// new String[]{android.Manifest.permission.CAMERA},
// REQUEST_CAMERA_PERMISSION);
// } else {
// startScanning();
// }
startScanning();
}
private void startScanning() {
barcodeView.getBarcodeView().setDecoderFactory(null);
barcodeView.decodeContinuous(barcodeCallback);
}
private BarcodeCallback barcodeCallback = new BarcodeCallback() {
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startScanning();
} else {
// 权限被拒绝的处理
}
}
}
BarcodeCallback barcodeCallback = new BarcodeCallback(){
@Override
public void barcodeResult(BarcodeResult result) {
if (result != null && result.getText() != null) {
if (result.getText() != null) {
//Toast.makeText(MainActivity.this, "Scanned: " + result.getText(), Toast.LENGTH_SHORT).show();
//ToastUtils.show("Scanned: " + result.getText());
barcodeView.pause();
String decodeResult = result.getText();
resultTextView.setText("扫码结果:" + decodeResult);
showDecodeResultDialog(decodeResult);
returnResultToPreviousPage(decodeResult);
Intent intent = new Intent();
intent.putExtra(EXTRA_RESULT, result.getText());
setResult(RESULT_OK, intent);
finish();
}
}
@@ -326,111 +92,16 @@ public class QRCodeDecodeActivity extends Activity {
}
};
private void showDecodeResultDialog(String result) {
ScrollView scrollView = new ScrollView(this);
scrollView.setPadding(dip2px(16), dip2px(16), dip2px(16), dip2px(16));
TextView dialogTv = new TextView(this);
dialogTv.setTextSize(16);
dialogTv.setTextColor(getResources().getColor(android.R.color.black));
dialogTv.setText(result);
scrollView.addView(dialogTv);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("解码结果");
builder.setView(scrollView);
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
barcodeView.resume();
}
});
builder.setCancelable(false);
builder.show();
}
private void returnResultToPreviousPage(String result) {
Intent intent = new Intent();
intent.putExtra(EXTRA_RESULT, result);
setResult(RESULT_OK, intent);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults != null && grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startScanning();
} else {
new AlertDialog.Builder(this)
.setTitle("权限申请")
.setMessage("扫码需要相机权限,请在设置中开启")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
finish();
}
})
.setCancelable(false)
.show();
}
}
} else if (requestCode == REQUEST_READ_STORAGE_PERMISSION) {
if (grantResults != null && grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openAlbum();
} else {
new AlertDialog.Builder(this)
.setTitle("权限申请")
.setMessage("从相册解码需要存储权限,请在设置中开启")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
}
}
}
}
@Override
protected void onResume() {
super.onResume();
if (barcodeView != null) {
barcodeView.resume();
}
barcodeView.resume();
}
@Override
protected void onPause() {
super.onPause();
if (barcodeView != null) {
barcodeView.pause();
}
}
private int dip2px(float dpValue) {
final float scale = getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showToast(String message) {
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).show();
barcodeView.pause();
}
}

View File

@@ -1,97 +0,0 @@
package cc.winboll.studio.apputils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/22 07:09
* @Describe 二维码生成窗口
*/
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.journeyapps.barcodescanner.BarcodeEncoder;
public class QRGeneratorActivity extends Activity {
public static final String TAG = "QrGeneratorActivity";
// 控件引用
private EditText etInputText;
private ImageView ivQrPreview;
private Button btnGenerateQr;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qrgenerator);
// 初始化控件
initViews();
// 设置按钮点击事件
setGenerateClickListener();
}
/**
* 初始化布局控件
*/
private void initViews() {
etInputText = findViewById(R.id.et_input_text);
ivQrPreview = findViewById(R.id.iv_qr_preview);
btnGenerateQr = findViewById(R.id.btn_generate_qr);
}
/**
* 设置生成按钮点击事件:获取输入文字 → 生成二维码 → 显示到 ImageView
*/
private void setGenerateClickListener() {
btnGenerateQr.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1. 获取输入框文字(去除前后空格)
String inputText = etInputText.getText().toString().trim();
// 2. 空输入判断
if (inputText.isEmpty()) {
Toast.makeText(QRGeneratorActivity.this, "请先输入要生成二维码的文字", Toast.LENGTH_SHORT).show();
return;
}
// 3. 生成二维码 Bitmap宽高 500px可调整
Bitmap qrBitmap = generateQrCodeBitmap(inputText, 500, 500);
// 4. 显示二维码到 ImageView
if (qrBitmap != null) {
ivQrPreview.setImageBitmap(qrBitmap);
} else {
Toast.makeText(QRGeneratorActivity.this, "二维码生成失败,请重试", Toast.LENGTH_SHORT).show();
}
}
});
}
/**
* 核心方法:生成二维码 Bitmap
* @param content 二维码内容(输入的文字)
* @param width 二维码宽度px
* @param height 二维码高度px
* @return 生成的二维码 Bitmap失败返回 null
*/
private Bitmap generateQrCodeBitmap(String content, int width, int height) {
try {
// 初始化二维码编码器(指定格式为 QR_CODE
BarcodeEncoder encoder = new BarcodeEncoder();
// 生成二维码 Bitmap参数内容、格式、宽、高
return encoder.encodeBitmap(content, BarcodeFormat.QR_CODE, width, height);
} catch (WriterException e) {
// 生成失败(如内容过长、宽高非法),打印异常信息
e.printStackTrace();
return null;
}
}
}

View File

@@ -6,9 +6,19 @@ package cc.winboll.studio.apputils;
* @Describe WinBoLLActivity
*/
import android.app.Activity;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
public class WinBoLLActivity extends Activity {
public class WinBoLLActivity extends Activity implements IWinBoLLActivity {
public static final String TAG = "WinBoLLActivity";
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
}

View File

@@ -54,6 +54,13 @@
</LinearLayout>
<cc.winboll.studio.libappbase.LogView
android:layout_width="match_parent"
android:layout_height="0dp"
android:text="Button"
android:id="@+id/logview"
android:layout_weight="1.0"/>
</LinearLayout>
</LinearLayout>

View File

@@ -1,43 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<!-- 工具栏(原有) -->
<Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<!-- 扫码控件(原有) -->
<com.journeyapps.barcodescanner.DecoratedBarcodeView
android:id="@+id/activityqrcodedecodeDecoratedBarcodeView1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<!-- 新增:从相册解码按钮 -->
<Button
android:id="@+id/btn_decode_from_album"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="从相册选图解码"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#2196F3"
android:paddingVertical="12dp"
android:layout_margin="16dp"/>
<!-- 结果显示TextView原有 -->
<TextView
android:id="@+id/activityqrcodedecodeTextView1"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="等待扫码或选择图片..."
android:textSize="16sp"/>
android:text="QRCodeDecodeActivity"/>
<com.journeyapps.barcodescanner.DecoratedBarcodeView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/activityqrcodedecodeDecoratedBarcodeView1"/>
</LinearLayout>

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp"
android:gravity="top|center_horizontal">
<!-- 文字输入框 -->
<EditText
android:id="@+id/et_input_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入要生成二维码的文字"
android:inputType="textMultiLine"
android:minLines="3"
android:maxLines="5"
android:padding="12dp"
android:background="@android:drawable/editbox_background_normal"
android:layout_marginBottom="16dp"/>
<!-- 生成二维码按钮 -->
<Button
android:id="@+id/btn_generate_qr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="生成二维码"
android:textSize="16sp"
android:background="#4CAF50"
android:textColor="@android:color/white"
android:paddingVertical="12dp"
android:layout_marginBottom="24dp"/>
<!-- 二维码预览图片 -->
<ImageView
android:id="@+id/iv_qr_preview"
android:layout_width="280dp"
android:layout_height="280dp"
android:scaleType="fitCenter"
android:background="@android:drawable/dialog_holo_light_frame"
android:contentDescription="二维码预览"/>
</LinearLayout>

View File

@@ -2,8 +2,8 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_testqrgeneratoractivity"
android:title="TestQRGeneratorActivity"/>
android:id="@+id/item_teststringtoqrcodeview"
android:title="TestStringToQRCodeViewActivity"/>
<item
android:id="@+id/item_testqrcodedecodeactivity"
android:title="TestQRCodeDecodeActivity"/>

View File

@@ -33,6 +33,9 @@ buildscript {
//println mavenLocal().url
//println "mavenLocal : ==========="
//mavenLocal()
// WinBoLL.CC 紧急备用 Maven 仓库
maven { url 'https://spare-maven.winboll.cc/repository/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1' // 对应 compileSdkVersion 32
@@ -56,6 +59,7 @@ allprojects {
password 'AKCp8ih1PFG9tV8qaLyws67dLGZi8udFM39SfsHgihN15cgsiRvHuxj8JzFmuZjaViVeNawaA'
}
}
// Nexus Maven 库地址
// "WinBoLL Release"
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
@@ -74,6 +78,9 @@ allprojects {
//println mavenLocal().url
//println "mavenLocal : ==========="
//mavenLocal()
// WinBoLL.CC 紧急备用 Maven 仓库
maven { url 'https://spare-maven.winboll.cc/repository/' }
}
ext {
// 定义全局变量,常用于版本管理

View File

@@ -18,8 +18,8 @@ def genVersionName(def versionName){
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
compileSdkVersion 32
buildToolsVersion "32.0.0"
defaultConfig {
applicationId "cc.winboll.studio.contacts"
@@ -66,7 +66,7 @@ dependencies {
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// 吐司类库
//api 'com.github.getActivity:ToastUtils:10.5'
api 'com.github.getActivity:ToastUtils:10.5'
// 网络连接类库
api 'com.squareup.okhttp3:okhttp:4.4.1'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Nov 03 12:01:02 HKT 2025
stageCount=22
#Sun Aug 31 06:05:42 CST 2025
stageCount=17
libraryProject=
baseVersion=15.3
publishVersion=15.3.21
publishVersion=15.3.16
buildCount=0
baseBetaVersion=15.3.22
baseBetaVersion=15.3.17

View File

@@ -7,8 +7,8 @@ package cc.winboll.studio.contacts;
*/
import android.view.Gravity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import cc.winboll.studio.libappbase.winboll.WinBoLLActivityManager;
import com.hjq.toast.ToastUtils;
public class App extends GlobalApplication {
@@ -30,7 +30,7 @@ public class App extends GlobalApplication {
// 设置 Toast 布局样式
//ToastUtils.setView(R.layout.toast_custom_view);
//ToastUtils.setStyle(new WhiteToastStyle());
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
}

View File

@@ -8,7 +8,6 @@ package cc.winboll.studio.contacts;
import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -39,10 +38,8 @@ import cc.winboll.studio.contacts.fragments.CallLogFragment;
import cc.winboll.studio.contacts.fragments.ContactsFragment;
import cc.winboll.studio.contacts.fragments.LogFragment;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.contacts.utils.AppGoToSettingsUtil;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
@@ -51,9 +48,10 @@ import java.util.List;
final public class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
public static final String TAG = "MainActivity";
public static final int REQUEST_HOME_ACTIVITY = 0;
public static final int REQUEST_ABOUT_ACTIVITY = 1;
public static final int REQUEST_APP_SETTINGS = 2;
public static final String ACTION_SOS = "cc.winboll.studio.libappbase.WinBoLL.ACTION_SOS";
static MainActivity _MainActivity;
@@ -74,13 +72,6 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
List<String> tabTitleList;
private static final int DIALER_REQUEST_CODE = 1;
private static final int REQUEST_REQUIRED_PERMISSIONS = 1002;
// 关键修改1新增 READ_CALL_LOG 权限到必需权限列表(解决通话记录读取崩溃)
private String[] REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.READ_CONTACTS, // 通讯录读取(原)
Manifest.permission.CALL_PHONE, // 电话拨号(原)
Manifest.permission.READ_CALL_LOG // 通话记录读取(新增,核心修复)
};
@Override
@@ -97,88 +88,9 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_MainActivity = this;
// 优先检查所有必需权限(含新增的 READ_CALL_LOG
if (!checkAllRequiredPermissions()) {
requestAllRequiredPermissions();
} else {
initUIAndLogic(savedInstanceState);
}
//ToastUtils.show("onCreate");
}
// 权限检查方法(无需修改,自动包含新增的 READ_CALL_LOG
private boolean checkAllRequiredPermissions() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ActivityCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
// 权限申请方法(无需修改,自动申请新增的 READ_CALL_LOG
private void requestAllRequiredPermissions() {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_REQUIRED_PERMISSIONS);
}
// 权限结果回调(无需修改,确保所有权限(含 READ_CALL_LOG都通过才加载UI
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_REQUIRED_PERMISSIONS) {
boolean allPermissionsGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allPermissionsGranted = false;
break;
}
}
if (allPermissionsGranted) {
initUIAndLogic(null);
} else {
// 关键修改2更新提示文案告知用户新增的“通话记录权限”
showPermissionDeniedDialogAndExit();
}
}
}
// 核心修改:新增“设置权限”按钮,点击调用 AppGoToSettingsUtil 跳转设置页
private void showPermissionDeniedDialogAndExit() {
new AlertDialog.Builder(this)
.setTitle("权限不足,无法使用")
// 文案修改:明确新增“通话记录读取”权限
.setMessage("应用需要「通讯录读取」、「电话」和「通话记录读取」权限才能正常运行,请授予权限后重新打开应用。")
.setCancelable(false)
// 新增:左侧“设置权限”按钮(先添加的按钮在左侧)
.setNegativeButton("设置权限", new AlertDialog.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
dialog.dismiss();
// 调用工具类跳转应用设置页(按需求实现)
AppGoToSettingsUtil appGoToSettingsUtil = new AppGoToSettingsUtil();
appGoToSettingsUtil.GoToSetting(MainActivity.this);
}
})
// 原有:右侧“确定退出”按钮(后添加的按钮在右侧)
.setPositiveButton("确定退出", new AlertDialog.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
dialog.dismiss();
finishAndRemoveTask();
}
})
.show();
}
// 初始化UI和逻辑无需修改权限通过后才加载 CallLogFragment
private void initUIAndLogic(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
// 初始化工具栏仅加载基础UI
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
setSupportActionBar(mToolbar);
getSupportActionBar().setSubtitle(TAG);
@@ -186,28 +98,34 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
tabLayout = (TabLayout) findViewById(R.id.tabLayout);
viewPager = (ViewPager) findViewById(R.id.viewPager);
// 创建Fragment列表仅实例化不加载数据
fragmentList = new ArrayList<Fragment>();
tabTitleList = new ArrayList<String>();
// CallLogFragment 仅在权限通过后才实例化(避免提前触发读取)
fragmentList.add(CallLogFragment.newInstance(0));
fragmentList.add(ContactsFragment.newInstance(1));
fragmentList.add(ContactsFragment.newInstance(1)); // 延迟加载联系人数据
fragmentList.add(LogFragment.newInstance(2));
tabTitleList.add("通话记录");
tabTitleList.add("联系人");
tabTitleList.add("应用日志");
// 设置ViewPager适配器
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
viewPager.setAdapter(adapter);
viewPager.setOffscreenPageLimit(0); // 关闭预加载,避免提前初始化 CallLogFragment
// 关键:关闭预加载,仅当前页初始化
viewPager.setOffscreenPageLimit(0);
// 关联TabLayout和ViewPager
tabLayout.setupWithViewPager(viewPager);
// 原有服务启动、电话监听等逻辑...
// 初始化服务状态(延迟启动非核心服务)
MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
if (mMainServiceBean == null) {
mMainServiceBean = new MainServiceBean();
MainServiceBean.saveBean(this, mMainServiceBean);
}
if (mMainServiceBean.isEnable()) {
// 延迟1秒启动服务避免阻塞启动
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
@@ -216,14 +134,16 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
}, 1000);
}
// 初始化电话状态监听(基础功能保留)
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
phoneStateListener = new MyPhoneStateListener();
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
// 以下为原有代码(无需修改
// ViewPager适配器Java 7语法
private class MyPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
private List<String> tabTitleList;
@@ -253,18 +173,21 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
if (ActivityCompat.checkSelfPermission(_MainActivity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show();
return;
}
_MainActivity.startActivity(intent);
}
// OnPageChangeListener接口实现
@Override
public void onPageScrollStateChanged(int state) {}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {}
@Override
public void onClick(View v) {}
@@ -316,6 +239,9 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
super.onResume();
}
/**
* 检查是否是系统默认电话应用
*/
public boolean isDefaultPhoneCallApp() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE);
@@ -346,9 +272,7 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用",
Toast.LENGTH_SHORT).show();
}
} else if (requestCode == REQUEST_APP_SETTINGS) {
recreate();
}
}
}
}

View File

@@ -36,8 +36,8 @@ import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.contacts.views.DuInfoTextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity;
import com.hjq.toast.ToastUtils;
import java.lang.reflect.Field;
import java.util.List;
@@ -263,7 +263,7 @@ public class SettingsActivity extends AppCompatActivity implements IWinBoLLActiv
@Override
public void run() {
if (tomCat.downloadBoBullToon()) {
LogUtils.d(TAG, "BoBullToon downlaod OK!");
ToastUtils.show("BoBullToon downlaod OK!");
MainService.restartMainService(SettingsActivity.this);
Rules.getInstance(SettingsActivity.this).reload();
}

View File

@@ -23,7 +23,7 @@ import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.beans.CallLogModel;
import cc.winboll.studio.contacts.utils.ContactUtils;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import com.hjq.toast.ToastUtils;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
@@ -76,9 +76,6 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV
// Set the clipboard's primary clip.
clipboard.setPrimaryClip(clip);
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
} else if (nItemId == R.id.item_calllog_phonenumber_add_contact) {
//ToastUtils.show(callLog.getPhoneNumber());
ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber());
}
return true;

View File

@@ -21,9 +21,8 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.beans.ContactModel;
import cc.winboll.studio.contacts.utils.ContactUtils;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import com.hjq.toast.ToastUtils;
import java.util.List;
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
@@ -70,11 +69,6 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV
// Set the clipboard's primary clip.
clipboard.setPrimaryClip(clip);
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
} else if (nItemId == R.id.item_calllog_phonenumber_edit_contact) {
//ToastUtils.show("Test");
Long nContactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber());
//ToastUtils.show(String.format("%d", nContactId));
ContactUtils.jumpToEditContact(mContext, contact.getNumber(), nContactId);
}
return true;
@@ -118,7 +112,7 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV
TextView contactName;
TextView contactNumber;
AOHPCTCSeekBar dialAOHPCTCSeekBar;
public ContactViewHolder(@NonNull View itemView) {
super(itemView);
llPhoneNumberMain = itemView.findViewById(R.id.itemcontactLinearLayout1);

View File

@@ -7,6 +7,7 @@ package cc.winboll.studio.contacts.adapters;
*/
import android.content.Context;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@@ -20,8 +21,9 @@ import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.contacts.views.LeftScrollView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import com.hjq.toast.ToastUtils;
import java.util.ArrayList;
import java.util.List;

View File

@@ -9,7 +9,7 @@ import android.content.Context;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import com.hjq.toast.ToastUtils;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;

View File

@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.adapters.CallLogAdapter;
import cc.winboll.studio.contacts.beans.CallLogModel;
import com.hjq.toast.ToastUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -160,7 +161,7 @@ public class CallLogFragment extends Fragment {
_CallLogFragment.triggerUpdate();
}
}
@Override
public void onResume() {
super.onResume();

View File

@@ -31,7 +31,7 @@ import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.adapters.ContactAdapter;
import cc.winboll.studio.contacts.beans.ContactModel;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import com.hjq.toast.ToastUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
@@ -95,7 +95,7 @@ public class ContactsFragment extends Fragment {
recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
contactList = new ArrayList<ContactModel>();
contactAdapter = new ContactAdapter(getActivity(), contactList);
contactAdapter = new ContactAdapter(getContext(), contactList);
recyclerView.setAdapter(contactAdapter);
// 初始隐藏列表,数据加载后显示
recyclerView.setVisibility(View.GONE);

View File

@@ -14,6 +14,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.libappbase.LogView;
import com.hjq.toast.ToastUtils;
public class LogFragment extends Fragment {

View File

@@ -10,7 +10,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import com.hjq.toast.ToastUtils;
import java.lang.ref.WeakReference;
public class MainReceiver extends BroadcastReceiver {

View File

@@ -1,270 +0,0 @@
package cc.winboll.studio.contacts.utils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/09/27 14:27
* @Describe 调用应用属性设置页工具类
* 来源https://blog.csdn.net/zhuhai__yizhi/article/details/78737593
* Created by zyy on 2018/3/12.
* 直接跳转到权限后返回,可以监控权限授权情况,但是,跳转到应用详情页,无法监测权限情况
* 是否要加以区分若是应用详情页则跳转回来后onRestart检测所求权限如果授权则收回提示如果没授权则继续提示
*/
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import cc.winboll.studio.contacts.MainActivity;
public class AppGoToSettingsUtil {
public static final String TAG = "AppGoToSettingsUtil";
public static final int ACTIVITY_RESULT_APP_SETTINGS = MainActivity.REQUEST_APP_SETTINGS;
/**
* Build.MANUFACTURER判断各大手机厂商品牌
*/
private static final String MANUFACTURER_HUAWEI = "Huawei";//华为
private static final String MANUFACTURER_MEIZU = "Meizu";//魅族
private static final String MANUFACTURER_XIAOMI = "Xiaomi";//小米
private static final String MANUFACTURER_SONY = "Sony";//索尼
private static final String MANUFACTURER_OPPO = "OPPO";
private static final String MANUFACTURER_LG = "LG";
private static final String MANUFACTURER_VIVO = "vivo";
private static final String MANUFACTURER_SAMSUNG = "samsung";//三星
private static final String MANUFACTURER_LETV = "Letv";//乐视
private static final String MANUFACTURER_ZTE = "ZTE";//中兴
private static final String MANUFACTURER_YULONG = "YuLong";//酷派
private static final String MANUFACTURER_LENOVO = "LENOVO";//联想
public static boolean isAppSettingOpen=false;
/**
* 跳转到相应品牌手机系统权限设置页,如果跳转不成功,则跳转到应用详情页
* 这里需要改造成返回true或者false应用详情页:true应用权限页:false
* @param activity
*/
public static void GoToSetting(Activity activity) {
switch (Build.MANUFACTURER) {
case MANUFACTURER_HUAWEI://华为
Huawei(activity);
break;
case MANUFACTURER_MEIZU://魅族
Meizu(activity);
break;
case MANUFACTURER_XIAOMI://小米
Xiaomi(activity);
break;
case MANUFACTURER_SONY://索尼
Sony(activity);
break;
case MANUFACTURER_OPPO://oppo
OPPO(activity);
break;
case MANUFACTURER_LG://lg
LG(activity);
break;
case MANUFACTURER_LETV://乐视
Letv(activity);
break;
default://其他
try {//防止应用详情页也找不到,捕获异常后跳转到设置,这里跳转最好是两级,太多用户也会觉得麻烦,还不如不跳
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
} catch (Exception e) {
SystemConfig(activity);
}
break;
}
}
/**
* 华为跳转权限设置页
* @param activity
*/
public static void Huawei(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
intent.setComponent(comp);
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = false;
} catch (Exception e) {
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
}
}
/**
* 魅族跳转权限设置页,测试时,点击无反应,具体原因不明
* @param activity
*/
public static void Meizu(Activity activity) {
try {
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("packageName", activity.getPackageName());
activity.startActivity(intent);
isAppSettingOpen = false;
} catch (Exception e) {
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
}
}
/**
* 小米,功能正常
* @param activity
*/
public static void Xiaomi(Activity activity) {
try { //MIUI 8 9
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
localIntent.putExtra("extra_pkgname", activity.getPackageName());
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = false;
//activity.startActivity(localIntent);
} catch (Exception e) {
try { //MIUI 5/6/7
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
localIntent.putExtra("extra_pkgname", activity.getPackageName());
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = false;
//activity.startActivity(localIntent);
} catch (Exception e1) { //否则跳转到应用详情
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
//这里有个问题,进入活动后需要再跳一级活动,就检测不到返回结果
//activity.startActivity(getAppDetailSettingIntent());
}
}
}
/**
* 索尼6.0以上的手机非常少,基本没看见
* @param activity
*/
public static void Sony(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
ComponentName comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity");
intent.setComponent(comp);
activity.startActivity(intent);
isAppSettingOpen = false;
} catch (Exception e) {
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
}
}
/**
* OPPO
* @param activity
*/
public static void OPPO(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
ComponentName comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity");
intent.setComponent(comp);
activity.startActivity(intent);
isAppSettingOpen = false;
} catch (Exception e) {
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
}
}
/**
* LG经过测试正常使用
* @param activity
*/
public static void LG(Activity activity) {
try {
Intent intent = new Intent("android.intent.action.MAIN");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
intent.setComponent(comp);
activity.startActivity(intent);
isAppSettingOpen = false;
} catch (Exception e) {
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
}
}
/**
* 乐视6.0以上很少,基本都可以忽略了,现在乐视手机不多
* @param activity
*/
public static void Letv(Activity activity) {
try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
ComponentName comp = new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps");
intent.setComponent(comp);
activity.startActivity(intent);
isAppSettingOpen = false;
} catch (Exception e) {
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
}
}
/**
* 只能打开到自带安全软件
* @param activity
*/
public static void _360(Activity activity) {
try {
Intent intent = new Intent("android.intent.action.MAIN");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packageName", activity.getPackageName());
ComponentName comp = new ComponentName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity");
intent.setComponent(comp);
activity.startActivity(intent);
} catch (Exception e) {
openAppDetailSetting(activity);
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
}
}
/**
* 系统设置界面
* @param activity
*/
public static void SystemConfig(Activity activity) {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivity(intent);
}
/**
* 获取应用详情页面
* @return
*/
private static Intent getAppDetailSettingIntent(Activity activity) {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//if (Build.VERSION.SDK_INT >= 9) {
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
localIntent.setData(Uri.fromParts("package", activity.getPackageName(), null));
/*} else if (Build.VERSION.SDK_INT <= 8) {
localIntent.setAction(Intent.ACTION_VIEW);
localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
localIntent.putExtra("com.android.settings.ApplicationPkgName", activity.getPackageName());
}*/
return localIntent;
}
public static void openAppDetailSetting(Activity activity) {
activity.startActivityForResult(getAppDetailSettingIntent(activity), ACTIVITY_RESULT_APP_SETTINGS);
isAppSettingOpen = true;
}
}

View File

@@ -6,14 +6,10 @@ package cc.winboll.studio.contacts.utils;
* @Describe 联系人工具集
*/
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import java.util.HashMap;
import java.util.Map;
@@ -124,92 +120,4 @@ public class ContactUtils {
}
return sbSpaceNumber.toString();
}
/**
* 跳转至系统添加联系人界面的工具函数
* @param context 上下文(如 PhoneCallService、Activity、Fragment 均可,需传入有效上下文)
* @param phoneNumber 可选参数:预填的联系人电话(传 null 则跳转空表单)
*/
public static void jumpToAddContact(Context mContext, String phoneNumber) {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setType("vnd.android.cursor.dir/person");
intent.putExtra(android.provider.ContactsContract.Intents.Insert.PHONE, phoneNumber);
mContext.startActivity(intent);
}
/**
* 跳转至系统编辑联系人界面(适配小米等定制机型)
* @param context 上下文Activity/Service/Fragment
* @param phoneNumber 待编辑联系人的电话号码(用于匹配已有联系人,必传)
* @param contactId 可选已有联系人的ID通过 ContactsContract 获取传null则自动匹配号码
*/
public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) {
Intent intent = new Intent(Intent.ACTION_EDIT);
// 关键:小米等机型需明确设置数据类型为“单个联系人”,避免参数丢失
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
// 场景A已知联系人ID精准定位优先用此方式参数传递最稳定
if (contactId != null && contactId > 0) {
// 构建联系人的Uri格式content://contacts/people/[contactId],系统标准格式)
Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
intent.setData(contactUri);
//ToastUtils.show("1");
} else if (phoneNumber != null && !phoneNumber.isEmpty()) {
// 方式1小米等机型兼容的“通过号码定位联系人”参数部分系统认此参数
//intent.putExtra(ContactsContract.Intents.Insert.PHONE_NUMBER, phoneNumber);
// 方式2补充系统标准的“数据Uri”强化匹配避免参数被定制系统忽略
Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
intent.setData(phoneUri);
} else {
LogUtils.d(TAG, "编辑联系人失败电话号码和联系人ID均为空");
return;
}
// 可选:预填最新号码(覆盖原有号码,若用户修改了号码,编辑时自动更新)
if (phoneNumber != null && !phoneNumber.isEmpty()) {
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
}
// 启动活动(加防护,避免无联系人应用崩溃)
// 小米机型在Service/非Activity中调用需加NEW_TASK标志否则可能无法启动
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* 通过电话号码查询联系人ID适配小米机型解决编辑时匹配不稳定问题
* @param context 上下文
* @param phoneNumber 待查询的电话号码
* @return 联系人ID无匹配时返回-1
*/
public static Long getContactIdByPhone(Context context, String phoneNumber) {
if (phoneNumber == null || phoneNumber.isEmpty()) {
return -1L;
}
ContentResolver cr = context.getContentResolver();
// 1. 构建电话查询Uri系统标准通过号码过滤联系人数据
Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
// 2. 只查询“联系人ID”字段高效避免冗余数据
String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
Cursor cursor = null;
try {
cursor = cr.query(queryUri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
// 3. 读取联系人ID返回Long类型避免int溢出
return cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID));
}
} catch (Exception e) {
LogUtils.d(TAG, "查询联系人ID失败。" + e);
} finally {
if (cursor != null) {
cursor.close(); // 关闭游标,避免内存泄漏
}
}
return -1L; // 无匹配联系人
}
}

View File

@@ -13,8 +13,9 @@ import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.threads.MainServiceThread;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.utils.ToastUtils;
import com.hjq.toast.ToastUtils;
public class APPStatusWidget extends AppWidgetProvider {

View File

@@ -5,8 +5,5 @@
<item
android:id="@+id/item_calllog_phonenumber_copy"
android:title="Copy"/>
<item
android:id="@+id/item_calllog_phonenumber_add_contact"
android:title="Add Contact"/>
</menu>

View File

@@ -5,8 +5,5 @@
<item
android:id="@+id/item_contact_phonenumber_copy"
android:title="Copy"/>
<item
android:id="@+id/item_calllog_phonenumber_edit_contact"
android:title="Edit Contact"/>
</menu>

View File

@@ -15,18 +15,12 @@ android {
minSdkVersion 23
targetSdkVersion 30
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
@@ -57,12 +51,12 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0'
// 米盟
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
implementation 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意以下5个库必须要引入
//implementation 'androidx.appcompat:appcompat:1.4.1'
api 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.code.gson:gson:2.8.5'
api 'com.github.bumptech.glide:glide:4.9.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
api 'cc.winboll.studio:libappbase:15.11.0'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Nov 26 15:54:26 GMT 2025
stageCount=7
#Wed Nov 19 09:04:27 HKT 2025
stageCount=5
libraryProject=libaes
baseVersion=15.11
publishVersion=15.11.6
buildCount=32
baseBetaVersion=15.11.7
publishVersion=15.11.4
buildCount=0
baseBetaVersion=15.11.5

View File

@@ -203,7 +203,7 @@ public abstract class DrawerFragmentActivity extends AppCompatActivity implement
ADsBannerView adsBannerView = findViewById(R.id.adsbanner);
if (adsBannerView != null) {
adsBannerView.resumeADs(DrawerFragmentActivity.this);
adsBannerView.resumeADs();
}
}

View File

@@ -1,31 +0,0 @@
package cc.winboll.studio.libaes.enums;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/26 17:49
* @Describe 广告控制模式枚举
*/
public enum ADsMode {
STANDALONE("单机模式"), // 单机模式(默认)
MIMO_SDK("米盟广告SDK支持模式"); // 米盟广告SDK模式
private final String modeName;
ADsMode(String modeName) {
this.modeName = modeName;
}
public String getModeName() {
return modeName;
}
// 根据保存的字符串值解析枚举SP读取时使用
public static ADsMode fromValue(String value) {
if (value == null) return STANDALONE;
try {
return ADsMode.valueOf(value);
} catch (IllegalArgumentException e) {
return STANDALONE;
}
}
}

View File

@@ -31,7 +31,6 @@ import com.miui.zeus.mimo.sdk.MimoSdk;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import cc.winboll.studio.libaes.enums.ADsMode;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -42,6 +41,8 @@ public class ADsBannerView extends LinearLayout {
public static final String TAG = "ADsBannerView";
private static final String PRIVACY_FILE = "privacy_pfs";
private static final String PRIVACY_VALUE = "privacy_value";//0: 拒绝1赞同
private String BANNER_POS_ID = "802e356f1726f9ff39c69308bfd6f06a";
private String BANNER_POS_ID_WINBOLL_BETA = "d129ee5a263911f981a6dc7a9802e3e7";
@@ -64,53 +65,63 @@ public class ADsBannerView extends LinearLayout {
public ADsBannerView(Context context) {
super(context);
initView(context);
this.mContext = context;
initView();
}
public ADsBannerView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
this.mContext = context;
initView();
}
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
this.mContext = context;
initView();
}
public ADsBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView(context);
this.mContext = context;
initView();
}
void initView(Context context) {
this.mContext = context;
initMimoSdk(this.mContext);
// 初始化主线程Handler关键确保广告操作在主线程执行
mMainHandler = new Handler(Looper.getMainLooper());
void initView() {
// 初始化主线程Handler关键确保广告操作在主线程执行
mMainHandler = new Handler(Looper.getMainLooper());
// 米盟模块:隐私协议弹窗
showPrivacy();
this.mMianView = inflate(this.mContext, R.layout.view_adsbanner, null);
mContainer = this.mMianView.findViewById(R.id.ads_container);
addView(this.mMianView);
}
public void resumeADs(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
Activity getActivity() {
try {
Activity activity = (Activity)this.mContext;
return activity;
} catch (Exception ex) {
}
return null;
}
public void resumeADs() {
// 修复:优化广告请求逻辑(添加生命周期判断 + 主线程执行)
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
if (ADsControlView.getAdsModeFromStatic(this.mContext) == ADsMode.MIMO_SDK) {
LogUtils.i(TAG, "已设置播放米盟广告,正在播放...");
if (getActivity() != null && !getActivity().isFinishing() && !getActivity().isDestroyed()) {
String privacyAgreeValue = getSharedPreferences().getString(PRIVACY_VALUE, null);
if (TextUtils.equals(privacyAgreeValue, String.valueOf(1))) {
LogUtils.i(TAG, "已同意隐私协议,开始播放米盟广告...");
mMainHandler.postDelayed(new Runnable() {
@Override
public void run() {
//ToastUtils.show("ADs run");
// 再次校验生命周期避免延迟执行时Activity已销毁
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
fetchAd(activity);
if (getActivity() != null && !getActivity().isFinishing() && !getActivity().isDestroyed()) {
fetchAd();
}
}
}, 1000); // 延迟1秒请求广告提升页面加载体验
@@ -122,11 +133,6 @@ public class ADsBannerView extends LinearLayout {
* 释放广告资源关键避免内存泄漏和空Context调用
*/
public void releaseAdResources() {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
LogUtils.d(TAG, "releaseAdResources()");
// 移除Handler回调
@@ -154,15 +160,10 @@ public class ADsBannerView extends LinearLayout {
/**
* 显示广告核心修复传递安全的Context + 生命周期校验)
*/
private void showAd(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
private void showAd() {
LogUtils.d(TAG, "showAd()");
// 1. 生命周期校验避免Activity已销毁时操作UI
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
if (getActivity() == null || getActivity().isFinishing() || getActivity().isDestroyed()) {
LogUtils.e(TAG, "showAd: Activity is finishing or destroyed");
return;
}
@@ -172,8 +173,8 @@ public class ADsBannerView extends LinearLayout {
return;
}
// 3. 创建广告容器使用ApplicationContext避免内存泄漏
final FrameLayout container = new FrameLayout(activity.getApplicationContext());
container.setPadding(0, 0, 0, MimoUtils.dpToPx(activity, 10));
final FrameLayout container = new FrameLayout(getActivity().getApplicationContext());
container.setPadding(0, 0, 0, MimoUtils.dpToPx(getActivity(), 10));
mContainer.addView(container, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
@@ -183,7 +184,7 @@ public class ADsBannerView extends LinearLayout {
// mBannerAd.setPrice(getPrice());
// }
// 4. 显示广告传递ApplicationContext避免Activity Context失效
mBannerAd.showAd(activity, container, new BannerAd.BannerInteractionListener() {
mBannerAd.showAd(getActivity(), container, new BannerAd.BannerInteractionListener() {
@Override
public void onAdClick() {
LogUtils.d(TAG, "onAdClick");
@@ -198,7 +199,7 @@ public class ADsBannerView extends LinearLayout {
public void onAdDismiss() {
LogUtils.d(TAG, "onAdDismiss");
// 修复移除容器时校验Activity状态
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
if (getActivity() != null && !getActivity().isFinishing() && !getActivity().isDestroyed() && mContainer != null) {
mContainer.removeView(container);
}
}
@@ -212,7 +213,7 @@ public class ADsBannerView extends LinearLayout {
public void onRenderFail(int code, String msg) {
LogUtils.e(TAG, "onRenderFail errorCode " + code + " errorMsg " + msg);
// 修复:渲染失败时移除容器
if (activity != null && !activity.isFinishing() && !activity.isDestroyed() && mContainer != null) {
if (getActivity() != null && !getActivity().isFinishing() && !getActivity().isDestroyed() && mContainer != null) {
mContainer.removeView(container);
}
}
@@ -222,15 +223,10 @@ public class ADsBannerView extends LinearLayout {
/**
* 请求广告核心修复Context安全校验 + 异常捕获 + 资源管理)
*/
private void fetchAd(final Activity activity) {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
private void fetchAd() {
LogUtils.d(TAG, "fetchAd()");
// 1. 双重校验Activity未销毁 + Context非空
if (activity == null || activity.isFinishing() || activity.isDestroyed() || activity.getApplicationContext() == null) {
if (getActivity() == null || getActivity().isFinishing() || getActivity().isDestroyed() || getActivity().getApplicationContext() == null) {
LogUtils.e(TAG, "fetchAd: Invalid Context or Activity state");
return;
}
@@ -305,9 +301,8 @@ public class ADsBannerView extends LinearLayout {
public void onBannerAdLoadSuccess() {
LogUtils.d(TAG, "onBannerAdLoadSuccess()");
// 修复广告加载成功后校验Activity状态
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
showAd(activity);
//ToastUtils.show("showAd()");
if (getActivity() != null && !getActivity().isFinishing() && !getActivity().isDestroyed()) {
showAd();
}
}
@@ -321,11 +316,6 @@ public class ADsBannerView extends LinearLayout {
}
void removeAllBanners() {
// 没有设置米盟广告支持就退出
if (ADsControlView.getAdsModeFromStatic(this.mContext) != ADsMode.MIMO_SDK) {
return;
}
// 修复:加载失败时移除当前广告实例
if (mAllBanners.contains(mBannerAd)) {
mAllBanners.remove(mBannerAd);
@@ -346,94 +336,91 @@ public class ADsBannerView extends LinearLayout {
/**
* 获取广告价格(原逻辑保留,添加空指针校验)
*/
// private long getPrice() {
// if (mBannerAd == null) {
// return 0;
// }
// Map<String, Object> map = mBannerAd.getMediaExtraInfo();
// if (map == null || map.isEmpty() || !map.containsKey("price")) {
// LogUtils.w(TAG, "getPrice: media extra info is null or no price key");
// return 0;
// }
// Object priceObj = map.get("price");
// if (priceObj instanceof Long) {
// return (Long) priceObj;
// } else if (priceObj instanceof Integer) {
// return ((Integer) priceObj).longValue();
// } else {
// LogUtils.e(TAG, "getPrice: price type is invalid");
// return 0;
// }
// }
private long getPrice() {
if (mBannerAd == null) {
return 0;
}
Map<String, Object> map = mBannerAd.getMediaExtraInfo();
if (map == null || map.isEmpty() || !map.containsKey("price")) {
LogUtils.w(TAG, "getPrice: media extra info is null or no price key");
return 0;
}
Object priceObj = map.get("price");
if (priceObj instanceof Long) {
return (Long) priceObj;
} else if (priceObj instanceof Integer) {
return ((Integer) priceObj).longValue();
} else {
LogUtils.e(TAG, "getPrice: price type is invalid");
return 0;
}
}
/**
* 显示隐私协议弹窗原逻辑保留优化Context使用
*/
// private void showPrivacy() {
// // 校验Activity状态避免弹窗泄露
// if (getActivity() == null || getActivity().isFinishing() || getActivity().isDestroyed()) {
// return;
// }
// ADsMode adsMode = ADsControlView.getAdsModeFromStatic(this.mContext);
// if (adsMode == ADsMode.STANDALONE) {
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.STANDALONE);
// LogUtils.i(TAG, "单机模式,广告已处于不可用状态...");
// Toast.makeText(getActivity().getApplicationContext(), "单机模式,广告已处于不可用状态...", Toast.LENGTH_SHORT).show();
// return;
// } else if (adsMode == ADsMode.MIMO_SDK) {
// ADsControlView.updateAdsModeByStatic(this.mContext, ADsMode.MIMO_SDK);
// LogUtils.i(TAG, "米盟广告SDK支持模式现在初始化SDK...");
// initMimoSdk();
// return;
// }
// else {
// LogUtils.i(TAG, "开始弹出隐私协议...");
// AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// builder.setTitle("用户须知");
// builder.setMessage("小米广告SDK隐私政策: https://dev.mi.com/distribute/doc/details?pId=1688, 请复制到浏览器查看");
// builder.setIcon(R.drawable.ic_launcher);
// builder.setCancelable(false); // 点击对话框以外的区域不消失
// builder.setPositiveButton("同意", new DialogInterface.OnClickListener() {
// @Override
// public void onClick(DialogInterface dialog, int which) {
// getSharedPreferences().edit()
// .putString(PRIVACY_VALUE, String.valueOf(1))
// .apply();
// initMimoSdk();
// dialog.dismiss();
// }
// });
// builder.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
// @Override
// public void onClick(DialogInterface dialog, int which) {
// getSharedPreferences().edit()
// .putString(PRIVACY_VALUE, String.valueOf(0))
// .apply();
// dialog.dismiss();
// }
// });
// AlertDialog dialog = builder.create();
//
// // 配置弹窗位置(底部全屏)
// Window window = dialog.getWindow();
// if (window != null) {
// window.setGravity(Gravity.BOTTOM);
// WindowManager m = getActivity().getWindowManager();
// Display d = m.getDefaultDisplay();
// WindowManager.LayoutParams p = window.getAttributes();
// p.width = d.getWidth();
// window.setAttributes(p);
// }
// dialog.show();
// }
// }
private void showPrivacy() {
// 校验Activity状态避免弹窗泄露
if (getActivity() == null || getActivity().isFinishing() || getActivity().isDestroyed()) {
return;
}
String privacyAgreeValue = getSharedPreferences().getString(PRIVACY_VALUE, null);
if (TextUtils.equals(privacyAgreeValue, String.valueOf(0))) {
LogUtils.i(TAG, "已拒绝隐私协议,广告已处于不可用状态...");
Toast.makeText(getActivity().getApplicationContext(), "已拒绝隐私协议,广告已处于不可用状态", Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.equals(privacyAgreeValue, String.valueOf(1))) {
LogUtils.i(TAG, "已同意隐私协议开始初始化米盟SDK...");
initMimoSdk();
return;
}
LogUtils.i(TAG, "开始弹出隐私协议...");
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("用户须知");
builder.setMessage("小米广告SDK隐私政策: https://dev.mi.com/distribute/doc/details?pId=1688, 请复制到浏览器查看");
builder.setIcon(R.drawable.ic_launcher);
builder.setCancelable(false); // 点击对话框以外的区域不消失
builder.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
getSharedPreferences().edit()
.putString(PRIVACY_VALUE, String.valueOf(1))
.apply();
initMimoSdk();
dialog.dismiss();
}
});
builder.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
getSharedPreferences().edit()
.putString(PRIVACY_VALUE, String.valueOf(0))
.apply();
dialog.dismiss();
}
});
AlertDialog dialog = builder.create();
// 配置弹窗位置(底部全屏)
Window window = dialog.getWindow();
if (window != null) {
window.setGravity(Gravity.BOTTOM);
WindowManager m = getActivity().getWindowManager();
Display d = m.getDefaultDisplay();
WindowManager.LayoutParams p = window.getAttributes();
p.width = d.getWidth();
window.setAttributes(p);
}
dialog.show();
}
/**
* 初始化米盟SDK核心修复传递ApplicationContext + 异常捕获)
*/
private void initMimoSdk(Context context) {
private void initMimoSdk() {
// 1. 安全获取ApplicationContext避免Activity Context失效
Context appContext = context.getApplicationContext();
Context appContext = getActivity().getApplicationContext();
if (appContext == null) {
LogUtils.e(TAG, "initMimoSdk: ApplicationContext is null");
return;
@@ -481,18 +468,18 @@ public class ADsBannerView extends LinearLayout {
/**
* 获取SharedPreferences实例原逻辑保留添加空指针校验
*/
// SharedPreferences getSharedPreferences() {
//// if (mSharedPreferences == null) {
//// // 修复使用ApplicationContext获取SharedPreferences避免Activity Context泄露
//// Context appContext = getActivity().getApplicationContext();
//// if (appContext != null) {
//// mSharedPreferences = appContext.getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
//// } else {
//// LogUtils.e(TAG, "getSharedPreferences: ApplicationContext is null");
//// // 降级方案若ApplicationContext为空使用Activity Context仅作兼容
//// mSharedPreferences = getActivity().getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
//// }
//// }
// return mSharedPreferences;
// }
SharedPreferences getSharedPreferences() {
if (mSharedPreferences == null) {
// 修复使用ApplicationContext获取SharedPreferences避免Activity Context泄露
Context appContext = getActivity().getApplicationContext();
if (appContext != null) {
mSharedPreferences = appContext.getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
} else {
LogUtils.e(TAG, "getSharedPreferences: ApplicationContext is null");
// 降级方案若ApplicationContext为空使用Activity Context仅作兼容
mSharedPreferences = getActivity().getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
}
}
return mSharedPreferences;
}
}

View File

@@ -1,484 +0,0 @@
package cc.winboll.studio.libaes.views;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import cc.winboll.studio.libaes.R;
import cc.winboll.studio.libaes.enums.ADsMode;
import cc.winboll.studio.libaes.views.ADsControlView;
import cc.winboll.studio.libappbase.LogUtils;
import com.miui.zeus.mimo.sdk.MimoCustomController;
import com.miui.zeus.mimo.sdk.MimoLocation;
import com.miui.zeus.mimo.sdk.MimoSdk;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/26 17:51
* @Describe 广告模式控制控件Java 7 兼容)
* 支持SP持久化、外部静态方法更新、Handler视图同步、外部静态方法读取
*/
public class ADsControlView extends LinearLayout {
public static final String TAG = "ADsControlView";
// SP存储配置
private static final String SP_NAME = "ads_control_config";
private static final String KEY_SELECTED_MODE = "selected_ads_mode";
// 单机模式与米盟模式标志位
ADsMode mADsMode;
private static final String PRIVACY_VALUE = "privacy_value";
// 隐私协议签约结果 0: 拒绝1赞同 2: 未签约
String privacyAgreeValue;
// Handler消息标识
private static final int MSG_UPDATE_MODE = 1001;
// 控件引用
private RadioGroup rgAdsMode;
private RadioButton rbStandalone;
private RadioButton rbMimoSdk;
// 外部监听、SP实例、Handler实例
private OnAdsModeSelectedListener listener;
private SharedPreferences sharedPreferences;
private InternalHandler mHandler;
private Context mContext;
// 静态列表存储所有已创建的控件实例Java 7 标准集合)
private static final java.util.List<ADsControlView> sControlViews = new java.util.ArrayList<ADsControlView>();
// 构造方法Java 7 兼容)
public ADsControlView(Context context) {
super(context);
initView(context);
}
public ADsControlView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
@SuppressWarnings("deprecation")
public ADsControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
public void setPrivacyAgreeValue(String privacyAgreeValue) {
this.privacyAgreeValue = privacyAgreeValue;
}
public String getPrivacyAgreeValue() {
String privacyAgreeValue = sharedPreferences.getString(PRIVACY_VALUE, "0");
return privacyAgreeValue;
}
public void setADsMode(ADsMode mADsMode) {
this.mADsMode = mADsMode;
sharedPreferences.edit().putString(KEY_SELECTED_MODE, this.mADsMode.name()).apply();
}
public ADsMode getADsMode() {
String savedModeStr = sharedPreferences.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
mADsMode = ADsMode.fromValue(savedModeStr);
return mADsMode;
}
/**
* 初始化视图、SP、Handler
*/
private void initView(final Context context) {
this.mContext = context;
// 加载布局
LayoutInflater.from(context).inflate(R.layout.view_adscontrol, this, true);
// 初始化SP
sharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
// 绑定控件
rgAdsMode = (RadioGroup) findViewById(R.id.rg_ads_mode);
rbStandalone = (RadioButton) findViewById(R.id.rb_standalone);
rbMimoSdk = (RadioButton) findViewById(R.id.rb_mimo_sdk);
// 初始化Handler主线程Looper
mHandler = new InternalHandler(Looper.getMainLooper());
// 注册控件实例到静态列表(线程安全)
registerControlView(this);
// 从SP读取初始模式并设置
ToastUtils.show(String.format("savedMode : %s", getADsMode().name()));
setSelectedMode(getADsMode());
// 单选组选择事件监听
rgAdsMode.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
if (checkedId == R.id.rb_standalone) {
setADsMode(ADsMode.STANDALONE);
} else if (checkedId == R.id.rb_mimo_sdk) {
showPrivacy(context, new OnPrivacyChangeListener(){
@Override
public void onAgreePrivacy() {
setADsMode(ADsMode.MIMO_SDK);
}
@Override
public void onDisagreePrivacy() {
setADsMode(ADsMode.STANDALONE);
}
});
}
}
});
}
/**
* 【静态】显示隐私协议弹窗供外部调用带Context参数
* @param context 上下文需传入Activity Context用于弹窗显示
*/
public static void showPrivacy(Context context, OnPrivacyChangeListener onPrivacyChangeListener) {
if (context == null) {
LogUtils.e(TAG, "showPrivacy: Context is null, cannot show privacy dialog");
return;
}
// 校验是否为Activity Context弹窗必须依附Activity
Activity activity = null;
try {
activity = (Activity) context;
} catch (ClassCastException e) {
LogUtils.e(TAG, "showPrivacy: Context is not Activity Context", e);
Toast.makeText(context.getApplicationContext(), "请传入Activity上下文以显示隐私协议", Toast.LENGTH_SHORT).show();
return;
}
// 校验Activity状态
if (activity.isFinishing() || activity.isDestroyed()) {
LogUtils.e(TAG, "showPrivacy: Activity is finishing or destroyed");
return;
}
// 读取隐私协议状态并处理逻辑
SbhhharedPreferences sp = getPrivacySharedPreferences(context);
String privacyAgreeValue = sp.getString(PRIVACY_VALUE, null);
handlePrivacyLogic(activity, privacyAgreeValue, onPrivacyChangeListener);
}
/**
* 【静态】清理SP中存储的隐私协议状态PRIVACY_VALUE
* 函数名cleanprivacystatus按要求命名
* @param context 上下文建议使用ApplicationContext
*/
public static void cleanPrivacyStatus(Context context) {
if (context == null) {
LogUtils.e(TAG, "cleanPrivacyStatus: Context is null, cannot clean privacy status");
return;
}
// 清空PRIVACY_VALUE值移除该键恢复初始未选择状态
SharedPreferences sp = getPrivacySharedPreferences(context);
sp.edit().remove(PRIVACY_VALUE).apply();
LogUtils.i(TAG, "cleanPrivacyStatus: Privacy status cleaned successfully");
ToastUtils.show("cleanPrivacyStatus: Privacy status cleaned successfully");
// 清理后同步更新广告模式为单机模式(避免隐私状态为空时仍加载广告)
//ADsControlView.updateAdsModeByStatic(context, ADsMode.STANDALONE);
}
// 【配套静态工具方法】获取隐私协议SP实例供上述两个静态方法调用需一并添加
private static SharedPreferences getPrivacySharedPreferences(Context context) {
// 使用ApplicationContext获取SP避免内存泄漏
Context appContext = context.getApplicationContext();
if (appContext != null) {
return appContext.getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
}
// 降级方案若ApplicationContext为空使用传入的Context
return context.getSharedPreferences(PRIVACY_FILE, Context.MODE_PRIVATE);
}
// 【配套静态工具方法】隐私协议逻辑处理(供上述两个静态方法调用,需一并添加)
private static void handlePrivacyLogic(final Activity activity, String privacyAgreeValue, final OnPrivacyChangeListener onPrivacyChangeListener) {
if (TextUtils.equals(privacyAgreeValue, String.valueOf(0))) {
ADsControlView.updateAdsModeByStatic(activity.getApplicationContext(), ADsMode.STANDALONE);
LogUtils.i(TAG, "已拒绝隐私协议,广告已处于不可用状态...");
Toast.makeText(activity.getApplicationContext(), "已拒绝隐私协议,广告已处于不可用状态", Toast.LENGTH_SHORT).show();
return;
} else if (TextUtils.equals(privacyAgreeValue, String.valueOf(1))) {
ADsControlView.updateAdsModeByStatic(activity.getApplicationContext(), ADsMode.MIMO_SDK);
LogUtils.i(TAG, "已同意隐私协议开始初始化米盟SDK...");
initMimoSdkStatic(activity.getApplicationContext());
return;
} else {
LogUtils.i(TAG, "开始弹出隐私协议...");
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle("用户须知");
builder.setMessage("小米广告SDK隐私政策: https://dev.mi.com/distribute/doc/details?pId=1688, 请复制到浏览器查看");
builder.setIcon(R.drawable.ic_launcher);
builder.setCancelable(false); // 点击对话框以外的区域不消失
builder.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SharedPreferences sp = getPrivacySharedPreferences(activity);
sp.edit().putString(PRIVACY_VALUE, String.valueOf(1)).apply();
initMimoSdkStatic(activity.getApplicationContext());
dialog.dismiss();
onPrivacyChangeListener.onAgreePrivacy();
}
});
builder.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SharedPreferences sp = getPrivacySharedPreferences(activity);
sp.edit().putString(PRIVACY_VALUE, String.valueOf(0)).apply();
ADsControlView.updateAdsModeByStatic(activity.getApplicationContext(), ADsMode.STANDALONE);
dialog.dismiss();
onPrivacyChangeListener.onDisagreePrivacy();
}
});
AlertDialog dialog = builder.create();
// 配置弹窗位置(底部全屏)
Window window = dialog.getWindow();
if (window != null) {
window.setGravity(Gravity.BOTTOM);
WindowManager m = activity.getWindowManager();
Display d = m.getDefaultDisplay();
WindowManager.LayoutParams p = window.getAttributes();
p.width = d.getWidth();
window.setAttributes(p);
}
dialog.show();
}
}
// 【配套静态工具方法】静态初始化米盟SDK供上述静态方法调用需一并添加
private static void initMimoSdkStatic(Context appContext) {
if (appContext == null) {
LogUtils.e(TAG, "initMimoSdkStatic: ApplicationContext is null");
return;
}
// 初始化SDK捕获异常避免崩溃
try {
MimoSdk.init(appContext, new MimoCustomController() {
@Override
public boolean isCanUseLocation() {
return true;
}
@Override
public MimoLocation getMimoLocation() {
return null;
}
@Override
public boolean isCanUseWifiState() {
return true;
}
@Override
public boolean alist() {
return true;
}
}, new MimoSdk.InitCallback() {
@Override
public void success() {
LogUtils.d(TAG, "MimoSdk init success (static)");
}
@Override
public void fail(int code, String msg) {
LogUtils.e(TAG, "MimoSdk init fail (static), code=" + code + ",msg=" + msg);
}
});
MimoSdk.setDebugOn(true);
} catch (Exception e) {
LogUtils.e(TAG, "initMimoSdkStatic: init failed", e);
}
}
/**
* 静态方法外部调用更新SP中的模式并发送消息通知控件更新
* @param context 上下文建议使用ApplicationContext
* @param mode 要设置的广告模式
*/
public static void updateAdsModeByStatic(Context context, ADsMode mode) {
if (context == null || mode == null) {
return;
}
// 1. 更新SP数据
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putString(KEY_SELECTED_MODE, mode.name()).apply();
// 2. 发送Handler消息通知所有控件更新
InternalHandler.sendUpdateModeMessage(mode);
}
/**
* 新增静态方法外部调用读取SP中存储的广告模式Java 7 兼容)
* @param context 上下文建议使用ApplicationContext
* @return 存储的AdsMode默认返回单机模式STANDALONE
*/
public static ADsMode getAdsModeFromStatic(Context context) {
// 空指针校验:上下文为空时返回默认模式
if (context == null) {
return ADsMode.STANDALONE;
}
// 读取SP数据
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String savedModeStr = sp.getString(KEY_SELECTED_MODE, ADsMode.STANDALONE.name());
// 解析枚举并返回兼容SP中数据异常的情况
return ADsMode.fromValue(savedModeStr);
}
/**
* 注册控件实例到静态列表(线程安全)
*/
private static void registerControlView(ADsControlView view) {
synchronized (sControlViews) {
if (!sControlViews.contains(view)) {
sControlViews.add(view);
}
}
}
/**
* 移除控件实例(线程安全)
*/
private static void unregisterControlView(ADsControlView view) {
synchronized (sControlViews) {
sControlViews.remove(view);
}
}
/**
* 设置选中模式内部使用更新UI
*/
private void setSelectedMode(final ADsMode mode) {
final ADsMode mode2;
if (mode == null) {
mode2 = ADsMode.STANDALONE;
} else {
mode2 = mode;
}
// 确保UI操作在主线程
if (Looper.myLooper() == Looper.getMainLooper()) {
if (mode2 == ADsMode.STANDALONE) {
rbStandalone.setChecked(true);
} else if (mode2 == ADsMode.MIMO_SDK) {
rbMimoSdk.setChecked(true);
}
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
setSelectedMode(mode2);
}
});
}
}
/**
* 获取当前选中模式
*/
public ADsMode getSelectedMode() {
int checkedId = rgAdsMode.getCheckedRadioButtonId();
return checkedId == R.id.rb_mimo_sdk ? ADsMode.MIMO_SDK : ADsMode.STANDALONE;
}
/**
* 设置外部监听
*/
public void setOnAdsModeSelectedListener(OnAdsModeSelectedListener listener) {
this.listener = listener;
}
/**
* 内部Handler类Java 7 静态内部类无隐藏API依赖
*/
private static class InternalHandler extends Handler {
static volatile InternalHandler _InternalHandler;
public InternalHandler(Looper looper) {
super(looper);
_InternalHandler = this;
}
/**
* 静态方法:发送模式更新消息
*/
public static void sendUpdateModeMessage(ADsMode mode) {
if (mode == null || _InternalHandler == null) {
return;
}
Message msg = _InternalHandler.obtainMessage();
msg.what = MSG_UPDATE_MODE;
msg.obj = mode;
_InternalHandler.sendMessage(msg);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_UPDATE_MODE) {
ADsMode mode = (ADsMode) msg.obj;
if (mode == null) {
return;
}
// 修复替换isDetachedFromWindow()为isAttachedToWindow()API 1兼容
// 逻辑调整view.isAttachedToWindow() → 控件已附加到窗口(活跃状态)
synchronized (sControlViews) {
for (ADsControlView view : sControlViews) {
// 三重校验:非空 + 可见 + 已附加到窗口(避免操作销毁/未初始化的控件)
if (view != null && view.isShown() && view.isAttachedToWindow()) {
view.setSelectedMode(mode);
}
}
}
}
}
}
/**
* 生命周期方法:控件销毁时解除注册,避免内存泄漏
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 移除Handler回调
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
// 从静态列表中移除当前控件
unregisterControlView(this);
}
/**
* 外部监听接口
*/
public interface OnAdsModeSelectedListener {
void onModeSelected(ADsMode selectedMode);
}
public interface OnPrivacyChangeListener {
void onAgreePrivacy();
void onDisagreePrivacy();
}
}

View File

@@ -193,9 +193,8 @@ public class AboutView extends LinearLayout {
elementGitWeb.setOnClickListener(mGitWebOnClickListener);
// 定义检查更新按钮
//
/*Element elementAppUpdate = new Element(_mContext.getString(R.string.app_update), R.drawable.ic_winboll);
Element elementAppUpdate = new Element(_mContext.getString(R.string.app_update), R.drawable.ic_winboll);
elementAppUpdate.setOnClickListener(mAppUpdateOnClickListener);
*/
String szAppInfo = "";
try {
@@ -215,8 +214,8 @@ public class AboutView extends LinearLayout {
//.addGroup("Connect with us")
.addEmail("ZhanGSKen<zhangsken@qq.com>")
.addWebsite(mszHomePage)
.addItem(elementGitWeb);
//.addItem(elementAppUpdate);
.addItem(elementGitWeb)
.addItem(elementAppUpdate);
//.addFacebook("the.medy")
//.addTwitter("medyo80")
//.addYoutube("UCdPQtdWIsg7_pi4mrRu46vA")
@@ -225,7 +224,7 @@ public class AboutView extends LinearLayout {
//.addInstagram("medyo80")
//.create();
/*if (mAPPInfo.isAddDebugTools()) {
if (mAPPInfo.isAddDebugTools()) {
// 定义应用调试按钮
//
Element elementAppMode;
@@ -237,7 +236,7 @@ public class AboutView extends LinearLayout {
elementAppMode.setOnClickListener(mAppDebugOnClickListener);
}
aboutPage.addItem(elementAppMode);
}*/
}
return aboutPage.create();
}

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@android:color/white">
<!-- 标题提示 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="运行模式选择"
android:textSize="16sp"
android:textColor="@android:color/black"
android:layout_marginBottom="12dp" />
<!-- 单选组 -->
<RadioGroup
android:id="@+id/rg_ads_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 单机模式单选框 -->
<RadioButton
android:id="@+id/rb_standalone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="单机模式"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="8dp" />
<!-- 米盟广告SDK模式单选框 -->
<RadioButton
android:id="@+id/rb_mimo_sdk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="米盟广告SDK支持模式"
android:textSize="14sp"
android:textColor="@android:color/darker_gray" />
</RadioGroup>
</LinearLayout>

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Nov 30 11:43:55 HKT 2025
stageCount=6
#Fri Nov 21 11:41:04 HKT 2025
stageCount=2
libraryProject=libappbase
baseVersion=15.11
publishVersion=15.11.5
publishVersion=15.11.1
buildCount=0
baseBetaVersion=15.11.6
baseBetaVersion=15.11.2

View File

@@ -3,9 +3,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libappbase">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<activity
android:name=".CrashHandler$CrashActivity"
android:label="CrashActivity"
@@ -29,21 +28,6 @@
</activity>
<!-- 崩溃通知复制动作接收活动(透明无界面) -->
<activity
android:name="cc.winboll.studio.libappbase.CrashCopyReceiverActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:taskAffinity=""
android:exported="true">
<!-- 注册复制动作的意图过滤器,接收通知按钮点击 -->
<intent-filter>
<action android:name="cc.winboll.studio.action.COPY_CRASH_LOG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -23,7 +23,6 @@ import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -186,17 +185,10 @@ public final class CrashHandler {
);
try {
if (GlobalApplication.isDebugging()) {
// 如果是 debug 版,启动崩溃页面窗口
app.startActivity(intent);
} else {
// 如果是 release 版,就只发送一个通知
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
}
// 终止当前进程(确保完全重启)
// 启动崩溃页面,终止当前进程(确保完全重启)
app.startActivity(intent);
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (ActivityNotFoundException e) {
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
e.printStackTrace();

View File

@@ -85,7 +85,6 @@ public class GlobalApplication extends Application {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
// 初始化基础组件日志、崩溃处理、Toast
initCoreComponents();
@@ -170,7 +169,6 @@ public class GlobalApplication extends Application {
// 释放单例引用(可选,避免内存泄漏风险)
sInstance = null;
LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
}
}

View File

@@ -0,0 +1,240 @@
package cc.winboll.studio.libappbase;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ListView;
import android.widget.Scroller;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:26
* @Describe 水平滚动 ListView 控件
* 继承自 ListView重写布局和测量逻辑实现子项水平排列和滚动替代默认垂直布局
*/
public class HorizontalListView extends ListView {
/** 日志标签,用于当前控件的日志输出标识 */
public static final String TAG = "HorizontalListView";
/** 子项垂直偏移量(用于调整子项在垂直方向的位置,默认 0 */
private int mVerticalOffset = 0;
/** 平滑滚动控制器(用于实现水平方向的平滑滚动动画) */
private Scroller mScroller;
/** 所有子项总宽度(包含内边距),用于计算滚动范围 */
private int mTotalWidth;
/**
* 构造方法:仅上下文
* @param context 上下文Activity/Fragment
*/
public HorizontalListView(Context context) {
super(context);
init();
}
/**
* 构造方法:上下文 + 自定义属性
* @param context 上下文
* @param attrs 自定义属性集合(如布局文件中设置的属性)
*/
public HorizontalListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 构造方法:上下文 + 自定义属性 + 样式属性
* @param context 上下文
* @param attrs 自定义属性集合
* @param defStyle 样式属性(如系统默认样式)
*/
public HorizontalListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* 初始化控件配置
* 初始化滚动控制器,设置滚动条显示状态
*/
private void init() {
// 初始化平滑滚动器(上下文为当前控件所在上下文)
mScroller = new Scroller(getContext());
// 启用水平滚动条(默认显示)
setHorizontalScrollBarEnabled(true);
// 禁用垂直滚动条(水平列表无需垂直滚动)
setVerticalScrollBarEnabled(false);
}
/**
* 设置子项垂直偏移量
* 用于整体调整所有子项在垂直方向的位置(如居中、偏移)
* @param verticalOffset 垂直偏移像素值(正数向下偏移,负数向上偏移)
*/
public void setVerticalOffset(int verticalOffset) {
this.mVerticalOffset = verticalOffset;
}
/**
* 重写布局方法:实现子项水平排列
* 遍历所有子项,按水平方向依次布局(左对齐,叠加排列)
* @param changed 布局是否发生变化true首次布局或尺寸变化false重绘
* @param l 控件左边界坐标
* @param t 控件上边界坐标
* @param r 控件右边界坐标
* @param b 控件下边界坐标
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b); // 执行父类布局逻辑(确保基础配置生效)
int childCount = getChildCount(); // 获取当前可见子项数量
int left = getPaddingLeft(); // 子项起始左坐标(包含控件左内边距)
// 控件可用高度(总高度 - 上下内边距)
int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
mTotalWidth = left; // 初始化总宽度为左内边距
// 遍历子项,水平排列
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i); // 获取当前子项
int childWidth = child.getMeasuredWidth(); // 子项测量宽度
int childHeight = child.getMeasuredHeight(); // 子项测量高度
// 布局子项:水平方向从 left 开始,垂直方向偏移 mVerticalOffset
child.layout(
left, // 子项左边界
mVerticalOffset, // 子项上边界(带垂直偏移)
left + childWidth, // 子项右边界(左 + 宽度)
mVerticalOffset + childHeight // 子项下边界(偏移 + 高度)
);
left += childWidth; // 更新下一个子项的起始左坐标
}
// 计算总宽度(所有子项宽度 + 左右内边距)
mTotalWidth = left + getPaddingRight();
}
/**
* 重写测量方法:设置控件测量规则
* 水平方向:允许无限宽度(适应所有子项总宽度);垂直方向:自适应内容高度
* @param widthMeasureSpec 父控件传递的宽度测量规格
* @param heightMeasureSpec 父控件传递的高度测量规格
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 重写宽度测量规则最大值Integer.MAX_VALUE >> 2 避免溢出),自适应内容
int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
// 重写高度测量规则:同上,自适应子项高度
int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
// 执行父类测量逻辑(使用重写后的测量规格)
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
}
/**
* 重写滚动计算方法:实现平滑滚动
* 配合 Scroller 实现水平方向的平滑滚动动画(需在滚动时调用 invalidate() 触发)
*/
@Override
public void computeScroll() {
// 判断滚动是否正在进行Scroller 计算当前滚动位置)
if (mScroller.computeScrollOffset()) {
// 滚动到当前计算的位置x 轴水平滚动y 轴固定 0
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 触发重绘,持续更新滚动状态
postInvalidate();
}
}
/**
* 平滑滚动到指定坐标
* 基于 Scroller 实现水平方向的平滑滚动300ms 动画时长)
* @param x 目标 x 轴坐标(水平滚动位置)
* @param y 目标 y 轴坐标(固定 0无需垂直滚动
*/
public void smoothScrollTo(int x, int y) {
// 计算滚动距离(目标坐标 - 当前滚动坐标)
int dx = x - getScrollX();
int dy = y - getScrollY();
// 启动平滑滚动起始坐标当前滚动位置、滚动距离、动画时长300ms
mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300);
// 触发重绘,启动滚动动画
invalidate();
}
/**
* 计算水平滚动总范围(用于滚动条显示比例)
* @return 滚动总宽度(所有子项总宽度 + 内边距)
*/
@Override
public int computeHorizontalScrollRange() {
return mTotalWidth;
}
/**
* 计算当前水平滚动偏移量(用于滚动条位置)
* @return 当前 x 轴滚动坐标
*/
@Override
public int computeHorizontalScrollOffset() {
return getScrollX();
}
/**
* 计算水平滚动可视范围(用于滚动条大小)
* @return 控件可见宽度(当前显示区域宽度)
*/
@Override
public int computeHorizontalScrollExtent() {
return getWidth();
}
/**
* 滚动到指定位置的子项(水平方向)
* 定位目标子项,计算滚动坐标,执行平滑滚动
* @param position 子项索引(从 0 开始,仅当前可见子项有效)
*/
public void scrollToItem(int position) {
// 校验索引有效性(避免数组越界)
if (position < 0 || position >= getChildCount()) {
LogUtils.d(TAG, "无效的子项索引: " + position);
return;
}
View targetView = getChildAt(position); // 获取目标子项
int targetLeft = targetView.getLeft(); // 目标子项左边界坐标
// 计算目标滚动坐标(子项左边界 - 控件左内边距,确保子项左对齐显示)
int scrollX = targetLeft - getPaddingLeft();
// 修正滚动范围(避免超出总宽度或小于 0
int maxScrollX = mTotalWidth - getWidth(); // 最大滚动坐标(总宽度 - 控件宽度)
scrollX = Math.max(0, Math.min(scrollX, maxScrollX));
// 强制重新布局和绘制(确保子项位置正确)
requestLayout();
invalidateViews();
// 平滑滚动到目标坐标
smoothScrollTo(scrollX, 0);
// 打印滚动日志(调试用)
LogUtils.d(TAG, String.format(
"滚动到子项索引: %d, 目标滚动X: %d, 总滚动范围: %d",
position, scrollX, computeHorizontalScrollRange()
));
}
/**
* 重置滚动到起始位置(最左侧)
* 强制重新布局后,平滑滚动到 x=0 坐标
*/
public void resetScrollToStart() {
// 强制重新布局和绘制(确保滚动位置准确)
requestLayout();
invalidateViews();
// 平滑滚动到最左侧x=0y=0
smoothScrollTo(0, 0);
}
}

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen@QQ.COM
* @Date 2024/08/12 14:36:18
* @Describe 日志视图类,继承 RelativeLayout 类。
*/
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -26,9 +21,6 @@ import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R;
import cc.winboll.studio.libappbase.views.HorizontalListView;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
@@ -36,27 +28,49 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/08/12 14:36:18
* @Describe 日志可视化自定义 View继承 RelativeLayout
* 核心功能日志展示、日志级别筛选、TAG 过滤(启用/禁用、TAG 搜索定位、日志清理/复制、视图交互控制
* 依赖 LogUtils 进行日志读写,通过 LogViewThread 监听日志文件变化并自动刷新
*/
public class LogView extends RelativeLayout {
/** 当前 View 的日志 TAG用于调试输出 */
public static final String TAG = "LogView";
public volatile boolean mIsHandling;
public volatile boolean mIsAddNewLog;
/** 日志处理中标志避免并发刷新volatile 保证多线程可见性) */
private volatile boolean mIsHandling;
/** 新日志添加标志标记有未处理的新日志volatile 保证多线程可见性) */
private volatile boolean mIsAddNewLog;
Context mContext;
ScrollView mScrollView;
TextView mTextView;
EditText metTagSearch;
CheckBox mSelectableCheckBox;
CheckBox mSelectAllTAGCheckBox;
TAGListAdapter mTAGListAdapter;
LogViewThread mLogViewThread;
LogViewHandler mLogViewHandler;
Spinner mLogLevelSpinner;
ArrayAdapter<CharSequence> mLogLevelSpinnerAdapter;
// 标签列表
HorizontalListView mListViewTags;
/** 上下文对象(用于布局加载、系统服务获取) */
private Context mContext;
/** 日志滚动视图(包裹日志文本,支持垂直滚动) */
private ScrollView mLogScrollView;
/** 日志文本展示控件(显示所有日志内容) */
private TextView mLogTextView;
/** TAG 搜索输入框(用于搜索并定位目标 TAG */
private EditText mTagSearchEt;
/** 文本选择开关(控制是否允许选中日志文本) */
private CheckBox mTextSelectableCb;
/** 全选 TAG 开关(控制所有 TAG 的启用/禁用) */
private CheckBox mSelectAllTagCb;
/** TAG 列表适配器(绑定 TAG 数据与视图,处理勾选状态) */
private TAGListAdapter mTagListAdapter;
/** 日志监听线程(监听日志文件变化,触发视图刷新) */
private LogViewThread mLogViewThread;
/** 日志视图 Handler主线程更新 UI避免跨线程操作 */
private LogViewHandler mLogViewHandler;
/** 日志级别选择下拉框(用于切换全局日志输出级别) */
private Spinner mLogLevelSpinner;
/** 日志级别适配器(绑定 LogUtils.LOG_LEVEL 枚举与 Spinner */
private ArrayAdapter<CharSequence> mLogLevelAdapter;
/** TAG 水平列表视图(横向展示所有 TAG支持滚动 */
private HorizontalListView mTagHorizontalListView;
// ====================== 构造方法(初始化视图) ======================
public LogView(Context context) {
super(context);
initView(context);
@@ -77,258 +91,307 @@ public class LogView extends RelativeLayout {
initView(context);
}
/**
* 启动日志监听与展示
* 1. 初始化并启动 LogViewThread监听日志文件变化
* 2. 初始加载并展示日志内容。
*/
public void start() {
mLogViewThread = new LogViewThread(LogView.this);
mLogViewThread = new LogViewThread(this);
mLogViewThread.start();
// 显示日志
showAndScrollLogView();
showAndScrollLogView(); // 初始显示日志并滚动到底部
}
public void scrollLogUp() {
mScrollView.post(new Runnable() {
@Override
public void run() {
mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
// 日志显示结束
mLogViewHandler.setIsHandling(false);
// 检查是否添加了新日志
if (mLogViewHandler.isAddNewLog()) {
// 有新日志添加,先更改新日志标志
mLogViewHandler.setIsAddNewLog(false);
// 再次发送显示日志的显示
Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
mLogViewHandler.sendMessage(message);
}
}
});
/**
* 滚动日志到底部(确保最新日志可见)
* 运行在主线程,通过 post 提交 Runnable 避免 UI 线程阻塞
*/
private void scrollLogToBottom() {
mLogScrollView.post(new Runnable() {
@Override
public void run() {
// 滚动到 ScrollView 底部FOCUS_DOWN 表示聚焦到底部)
mLogScrollView.fullScroll(ScrollView.FOCUS_DOWN);
// 标记日志处理完成
mLogViewHandler.setIsHandling(false);
// 检查是否有未处理的新日志,有则再次触发刷新
if (mLogViewHandler.isAddNewLog()) {
mLogViewHandler.setIsAddNewLog(false);
Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH);
mLogViewHandler.sendMessage(refreshMsg);
}
}
});
}
void initView(Context context) {
/**
* 初始化视图组件(加载布局、绑定控件、设置监听)
* @param context 上下文对象
*/
private void initView(Context context) {
mContext = context;
mLogViewHandler = new LogViewHandler();
// 加载视图布局
addView(inflate(mContext, cc.winboll.studio.libappbase.R.layout.view_log, null));
// 初始化日志子控件视图
//
mScrollView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogScrollViewLog);
mTextView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogTextViewLog);
metTagSearch = findViewById(cc.winboll.studio.libappbase.R.id.tagsearch_et);
// 获取Log Level spinner实例
mLogLevelSpinner = findViewById(cc.winboll.studio.libappbase.R.id.viewlogSpinner1);
mLogViewHandler = new LogViewHandler(); // 初始化主线程 Handler
metTagSearch.addTextChangedListener(new TextWatcher() {
// 加载日志视图布局R.layout.view_log 为自定义布局文件)
View rootView = LayoutInflater.from(context).inflate(R.layout.view_log, this, true);
// 绑定布局控件(通过 ID 找到对应组件)
bindViews(rootView);
@Override
public void afterTextChanged(Editable editable) {
}
@Override
public void beforeTextChanged(CharSequence charSequence, int p, int p1, int p2) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
LogUtils.d(TAG, s.toString());
if (s.length() > 0) {
scrollToTag(s.toString());
} else {
HorizontalScrollView hsRoot = findViewById(R.id.viewlogHorizontalScrollView1);
hsRoot.smoothScrollTo(0, 0);
mListViewTags.resetScrollToStart();
}
// mListViewTags.postDelayed(new Runnable() {
// @Override
// public void run() {
// mListViewTags.scrollToItem(5);
// }
// }, 100);
}
// 其他方法留空或按需实现
});
(findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonClean)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
LogUtils.cleanLog();
LogUtils.d(TAG, "Log is cleaned.");
}
});
(findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonCopy)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
ClipboardManager cm = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
LogUtils.d(TAG, "Log is copied.");
}
});
mSelectableCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBoxSelectable);
mSelectableCheckBox.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
if (mSelectableCheckBox.isChecked()) {
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
} else {
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
}
});
// 设置日志级别列表
ArrayList<String> adapterItems = new ArrayList<>();
for (LogUtils.LOG_LEVEL e : LogUtils.LOG_LEVEL.values()) {
adapterItems.add(e.name());
}
// 假设你有一个字符串数组作为选项列表
//String[] options = {"Option 1", "Option 2", "Option 3"};
// 创建一个ArrayAdapter来绑定数据到spinner
mLogLevelSpinnerAdapter = ArrayAdapter.createFromResource(
context, cc.winboll.studio.libappbase.R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// 设置适配器并将它应用到spinner上
mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // 设置下拉视图样式
mLogLevelSpinner.setAdapter(mLogLevelSpinnerAdapter);
// 为Spinner添加监听器
mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
//String selectedOption = mLogLevelSpinnerAdapter.getItem(position);
// 处理选中的选项...
LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// 如果没有选择,则执行此操作...
}
});
// 获取默认值的索引
int defaultValueIndex = LogUtils.getLogLevel().ordinal();
if (defaultValueIndex != -1) {
// 如果找到了默认值,设置默认选项
mLogLevelSpinner.setSelection(defaultValueIndex);
}
// 加载标签列表
Map<String, Boolean> mapTAGList = LogUtils.getMapTAGList();
boolean isAllSelect = true;
for (Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
if (entry.getValue() == false) {
isAllSelect = false;
break;
}
}
CheckBox cbALLTAG = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
cbALLTAG.setChecked(isAllSelect);
// 加载标签表
mListViewTags = findViewById(cc.winboll.studio.libappbase.R.id.tags_listview);
mListViewTags.setVerticalOffset(10);
mTAGListAdapter = new TAGListAdapter(mContext, mapTAGList);
mListViewTags.setAdapter(mTAGListAdapter);
// 可以添加点击监听器来处理勾选框状态变化后的逻辑,比如获取当前勾选情况等
mTAGListAdapter.notifyDataSetChanged();
mSelectAllTAGCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
mSelectAllTAGCheckBox.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
LogUtils.setALlTAGListEnable(mSelectAllTAGCheckBox.isChecked());
//LogUtils.setALlTAGListEnable(false);
//mTAGListAdapter.notifyDataSetChanged();
mTAGListAdapter.reload();
//ToastUtils.show(String.format("onClick\nmSelectAllTAGCheckBox.isChecked() : %s", mSelectAllTAGCheckBox.isChecked()));
}
});
// 设置滚动时不聚焦日志
// 设置 TAG 搜索输入框监听(实时搜索并定位 TAG
setupTagSearchListener();
// 设置功能按钮监听(清理日志、复制日志)
setupFunctionButtonListeners(rootView);
// 设置文本选择开关监听(控制日志文本是否可选中)
setupTextSelectableListener();
// 初始化日志级别下拉框(绑定级别数据,设置默认值)
initLogLevelSpinner();
// 初始化 TAG 列表(加载所有 TAG设置全选状态
initTagListView();
// 设置默认交互模式(默认禁止子视图获取焦点,避免误触)
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
/**
* 绑定布局控件(通过 ID 查找并初始化所有子组件)
* @param rootView 根布局视图
*/
private void bindViews(View rootView) {
mLogScrollView = rootView.findViewById(R.id.viewlogScrollViewLog);
mLogTextView = rootView.findViewById(R.id.viewlogTextViewLog);
mTagSearchEt = rootView.findViewById(R.id.tagsearch_et);
mLogLevelSpinner = rootView.findViewById(R.id.viewlogSpinner1);
mTextSelectableCb = rootView.findViewById(R.id.viewlogCheckBoxSelectable);
mSelectAllTagCb = rootView.findViewById(R.id.viewlogCheckBox1);
mTagHorizontalListView = rootView.findViewById(R.id.tags_listview);
}
/**
* 设置 TAG 搜索输入框监听(文本变化时触发 TAG 定位)
*/
private void setupTagSearchListener() {
mTagSearchEt.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) {
String searchText = s.toString().trim();
LogUtils.d(TAG, "TAG 搜索内容:" + searchText);
if (!searchText.isEmpty()) {
// 搜索文本非空,定位匹配的 TAG
scrollToTargetTag(searchText);
} else {
// 搜索文本为空,重置滚动位置
HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1);
parentHs.smoothScrollTo(0, 0);
mTagHorizontalListView.resetScrollToStart();
}
}
@Override
public void afterTextChanged(Editable s) {}
});
}
/**
* 设置功能按钮监听(清理日志、复制日志)
*/
private void setupFunctionButtonListeners(View rootView) {
// 清理日志按钮(点击清空所有历史日志)
rootView.findViewById(R.id.viewlogButtonClean).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.cleanLog();
LogUtils.d(TAG, "日志已清理");
}
});
// 复制日志按钮(点击复制所有日志到剪贴板)
rootView.findViewById(R.id.viewlogButtonCopy).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
// 将日志内容复制到剪贴板(标签为应用包名)
clipboard.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
LogUtils.d(TAG, "日志已复制到剪贴板");
}
});
}
/**
* 设置文本选择开关监听(控制日志文本是否可选中复制)
*/
private void setupTextSelectableListener() {
mTextSelectableCb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mTextSelectableCb.isChecked()) {
// 允许文本选择:子视图优先获取焦点
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
} else {
// 禁止文本选择:阻止子视图获取焦点
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
}
});
}
/**
* 初始化日志级别下拉框Spinner
* 1. 绑定 LogUtils.LOG_LEVEL 枚举数据;
* 2. 设置默认选中当前全局日志级别;
* 3. 监听级别变化,更新 LogUtils 全局配置。
*/
private void initLogLevelSpinner() {
// 从资源文件加载日志级别数组R.array.enum_loglevel_array 与 LOG_LEVEL 枚举对应)
mLogLevelAdapter = ArrayAdapter.createFromResource(
mContext, R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
// 设置下拉列表样式
mLogLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mLogLevelSpinner.setAdapter(mLogLevelAdapter);
// 监听下拉框选择变化
mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// 根据选择的位置设置全局日志级别position 与 LOG_LEVEL 枚举索引对应)
LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
// 设置默认选中当前日志级别
int defaultLevelIndex = LogUtils.getLogLevel().ordinal();
if (defaultLevelIndex >= 0) {
mLogLevelSpinner.setSelection(defaultLevelIndex);
}
}
/**
* 初始化 TAG 水平列表
* 1. 加载 LogUtils 中的所有 TAG 及其启用状态;
* 2. 初始化 TAG 列表适配器;
* 3. 设置全选 TAG 开关监听。
*/
private void initTagListView() {
// 获取 LogUtils 中的 TAG 启用状态映射表
Map<String, Boolean> tagEnableMap = LogUtils.getTagEnableMap();
// 判断是否所有 TAG 都已启用(初始化全选开关状态)
boolean isAllTagEnabled = isAllTagsEnabled(tagEnableMap);
mSelectAllTagCb.setChecked(isAllTagEnabled);
// 初始化 TAG 水平列表(设置垂直偏移,绑定适配器)
mTagHorizontalListView.setVerticalOffset(10);
mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap);
mTagHorizontalListView.setAdapter(mTagListAdapter);
mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据
// 全选 TAG 开关监听(点击时启用/禁用所有 TAG
mSelectAllTagCb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean isSelectAll = mSelectAllTagCb.isChecked();
LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态
mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图
}
});
}
/**
* 判断是否所有 TAG 都已启用
* @param tagEnableMap TAG 启用状态映射表
* @return true所有 TAG 均启用false存在未启用的 TAG
*/
private boolean isAllTagsEnabled(Map<String, Boolean> tagEnableMap) {
for (Map.Entry<String, Boolean> entry : tagEnableMap.entrySet()) {
if (!entry.getValue()) {
return false;
}
}
return true;
}
/**
* 更新日志视图(由 LogViewThread 触发,通知有新日志)
* 避免并发刷新:正在处理时标记新日志,处理完成后再次刷新
*/
public void updateLogView() {
if (mLogViewHandler.isHandling() == true) {
// 正在处理日志显示,
// 就先设置一个新日志标志位
// 以便日志显示完后,再次显示新日志内容
if (mLogViewHandler.isHandling()) {
// 正在处理日志刷新,标记有新日志待处理
mLogViewHandler.setIsAddNewLog(true);
} else {
//LogUtils.d(TAG, "LogListener showLog(String path)");
Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
mLogViewHandler.sendMessage(message);
// 发送刷新消息到主线程
Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH);
mLogViewHandler.sendMessage(refreshMsg);
mLogViewHandler.setIsAddNewLog(false);
}
}
void showAndScrollLogView() {
mTextView.setText(LogUtils.loadLog());
scrollLogUp();
/**
* 显示日志并滚动到底部
* 1. 从 LogUtils 加载所有历史日志;
* 2. 设置到文本控件并滚动到底部。
*/
private void showAndScrollLogView() {
mLogTextView.setText(LogUtils.loadLog()); // 加载并显示日志
scrollLogToBottom(); // 滚动到底部,显示最新日志
}
public void scrollToTag(final String prefix) {
if (mTAGListAdapter == null || prefix == null || prefix.length() == 0) {
LogUtils.d(TAG, "参数为空,无法滚动");
/**
* 滚动到目标 TAG根据搜索文本定位匹配的 TAG 并滚动显示)
* @param prefix 搜索文本TAG 前缀)
*/
private void scrollToTargetTag(final String prefix) {
if (mTagListAdapter == null || prefix == null || prefix.isEmpty()) {
LogUtils.d(TAG, "TAG 搜索参数为空,无法定位");
return;
}
final List<TAGItemModel> itemList = mTAGListAdapter.getItemList();
final List<TAGItemModel> tagItemList = mTagListAdapter.getItemList();
mTagHorizontalListView.post(new Runnable() {
@Override
public void run() {
int targetPosition = -1;
// 遍历 TAG 列表,查找前缀匹配的 TAG忽略大小写
for (int i = 0; i < tagItemList.size(); i++) {
String tag = tagItemList.get(i).getTag();
if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
targetPosition = i;
break;
}
}
mListViewTags.post(new Runnable() {
@Override
public void run() {
// 查找匹配的标签位置
int targetPosition = -1;
for (int i = 0; i < itemList.size(); i++) {
String tag = itemList.get(i).getTag();
if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
targetPosition = i;
break;
}
}
if (targetPosition != -1) {
// 优化滚动逻辑
//mListViewTags.setSelection(targetPosition);
//mListViewTags.invalidateViews(); // 强制刷新所有可见项
// 单独刷新目标视图
// View targetView = mListViewTags.getChildAt(targetPosition);
// if (targetView != null) {
// targetView.requestLayout();
// targetView.requestFocus();
// }
final int scrollPosition = targetPosition;
// 延迟滚动确保布局完成
mListViewTags.postDelayed(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, String.format("scrollPosition %d", scrollPosition));
mListViewTags.scrollToItem(scrollPosition);
}
}, 100);
} else {
LogUtils.d(TAG, "未找到匹配的标签前缀:" + prefix);
}
}
});
if (targetPosition != -1) {
final int targetPositionFinal = targetPosition;
// 延迟滚动(确保布局完成,避免滚动失效)
mTagHorizontalListView.postDelayed(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, "定位到 TAG 位置:" + targetPositionFinal);
mTagHorizontalListView.scrollToItem(targetPositionFinal);
}
}, 100);
} else {
LogUtils.d(TAG, "未找到匹配前缀的 TAG" + prefix);
}
}
});
}
class LogViewHandler extends Handler {
final static int MSG_LOGVIEW_UPDATE = 0;
volatile boolean isHandling;
volatile boolean isAddNewLog;
// ====================== 内部类:日志视图 Handler主线程更新 UI ======================
/**
* 日志视图 Handler运行在主线程处理日志刷新消息
* 避免跨线程操作 UI通过标志位控制并发刷新
*/
private class LogViewHandler extends Handler {
/** 日志刷新消息标识 */
private static final int MSG_LOG_REFRESH = 0;
/** 日志处理中标志(与外部 mIsHandling 同步) */
private volatile boolean isHandling;
/** 新日志添加标志(与外部 mIsAddNewLog 同步) */
private volatile boolean isAddNewLog;
public LogViewHandler() {
setIsHandling(false);
@@ -351,24 +414,32 @@ public class LogView extends RelativeLayout {
return isAddNewLog;
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_LOGVIEW_UPDATE:{
if (isHandling() == false) {
setIsHandling(true);
showAndScrollLogView();
}
break;
case MSG_LOG_REFRESH:
// 未处理日志刷新时,标记为处理中并触发显示
if (!isHandling()) {
setIsHandling(true);
showAndScrollLogView();
}
break;
default:
break;
}
super.handleMessage(msg);
}
}
public class TAGItemModel {
// ====================== 内部类TAG 数据模型(封装 TAG 名称与状态) ======================
/**
* TAG 列表项数据模型
* 封装单个 TAG 的名称及其启用状态(用于 Adapter 数据绑定)
*/
private class TAGItemModel {
/** TAG 名称(如 "LogViewThread"、"LogUtils" */
private String tag;
/** TAG 启用状态true启用false禁用 */
private boolean isChecked;
public TAGItemModel(String tag, boolean isChecked) {
@@ -392,18 +463,17 @@ public class LogView extends RelativeLayout {
isChecked = checked;
}
// getter/setter...
/**
* 重写 equals 方法(按 TAG 名称判断相等)
* @param o 比较对象
* @return trueTAG 名称相同false不同
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TAGItemModel that = (TAGItemModel) o;
// 手动处理空值比较Java 6 不支持 Objects.equals
// 手动处理空值比较(兼容 Java 7不依赖 Objects.equals
if (tag == null) {
return that.tag == null;
} else {
@@ -411,106 +481,174 @@ public class LogView extends RelativeLayout {
}
}
/**
* 重写 hashCode 方法(基于 TAG 名称生成哈希值)
* @return 哈希值(空 TAG 返回 0
*/
@Override
public int hashCode() {
return tag == null ? 0 : tag.hashCode(); // 手动处理空值
return tag == null ? 0 : tag.hashCode();
}
}
public class TAGListAdapter extends BaseAdapter {
// ====================== 内部类TAG 列表适配器(绑定数据与视图) ======================
/**
* TAG 水平列表适配器(继承 BaseAdapter
* 负责 TAG 数据与列表项视图的绑定,处理勾选状态变化
*/
private class TAGListAdapter extends BaseAdapter {
/** 上下文对象(用于加载列表项布局) */
private Context context;
private Map<String, Boolean> mapOrigin;
private List<TAGItemModel> itemList;
/** 原始 TAG 启用状态映射表(来自 LogUtils */
private Map<String, Boolean> originTagMap;
/** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */
private List<TAGItemModel> tagItemList;
public TAGListAdapter(Context context, Map<String, Boolean> map) {
/**
* 构造方法(初始化数据并加载到列表)
* @param context 上下文
* @param tagMap TAG 启用状态映射表
*/
public TAGListAdapter(Context context, Map<String, Boolean> tagMap) {
this.context = context;
mapOrigin = map;
loadMap(mapOrigin);
this.originTagMap = tagMap;
loadTagData(originTagMap); // 加载并转换数据
}
/**
* 获取 TAG 列表项数据(供外部定位 TAG 使用)
* @return TAGItemModel 列表
*/
public List<TAGItemModel> getItemList() {
return itemList;
return tagItemList;
}
// ====================== BaseAdapter 抽象方法实现 ======================
@Override
public int getCount() {
return itemList.size();
return tagItemList == null ? 0 : tagItemList.size();
}
@Override
public Object getItem(int p) {
return itemList.get(p);
public Object getItem(int position) {
return tagItemList.get(position);
}
@Override
public long getItemId(int p) {
return p;
public long getItemId(int position) {
return position;
}
void loadMap(Map<String, Boolean> map) {
itemList = new ArrayList<TAGItemModel>();
for (Map.Entry<String, Boolean> entry : map.entrySet()) {
itemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
/**
* 加载 TAG 数据(将 Map 转换为 List 并排序)
* @param tagMap TAG 启用状态映射表
*/
private void loadTagData(Map<String, Boolean> tagMap) {
tagItemList = new ArrayList<>();
// 遍历 Map转换为 TAGItemModel 并添加到列表
for (Map.Entry<String, Boolean> entry : tagMap.entrySet()) {
tagItemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
}
// 添加排序功能按照tag进行升序排序
Collections.sort(itemList, new SortMapEntryByKeyString(true));
//Collections.sort(itemList, new SortMapEntryByKeyString(false));
// 按 TAG 名称升序排序(中文排序兼容)
Collections.sort(tagItemList, new TagAscComparator(true));
}
/**
* 重新加载 TAG 数据(用于全选/反选后刷新列表)
*/
public void reload() {
loadMap(mapOrigin);
super.notifyDataSetChanged();
loadTagData(originTagMap); // 重新加载数据
notifyDataSetChanged(); // 通知视图刷新
}
/**
* 创建/复用列表项视图(优化性能,避免重复 inflate
* @param position 列表项位置
* @param convertView 复用视图(可为 null
* @param parent 父容器
* @return 列表项视图
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
// 复用视图(减少布局加载开销)
if (convertView == null) {
// 加载列表项布局R.layout.item_logtag 为 TAG 项自定义布局)
convertView = LayoutInflater.from(context).inflate(R.layout.item_logtag, parent, false);
holder = new ViewHolder();
holder.tvText = convertView.findViewById(R.id.viewlogtagTextView1);
holder.cbChecked = convertView.findViewById(R.id.viewlogtagCheckBox1);
convertView.setTag(holder);
// 绑定列表项控件TAG 文本和勾选框)
holder.tagTv = convertView.findViewById(R.id.viewlogtagTextView1);
holder.tagCb = convertView.findViewById(R.id.viewlogtagCheckBox1);
convertView.setTag(holder); // 保存 ViewHolder 到视图
} else {
holder = (ViewHolder) convertView.getTag();
holder = (ViewHolder) convertView.getTag(); // 复用 ViewHolder
}
final TAGItemModel item = itemList.get(position);
holder.tvText.setText(item.getTag());
holder.cbChecked.setChecked(item.isChecked());
holder.cbChecked.setOnClickListener(new View.OnClickListener(){
// 绑定数据到视图
final TAGItemModel item = tagItemList.get(position);
holder.tagTv.setText(item.getTag()); // 设置 TAG 名称
holder.tagCb.setChecked(item.isChecked()); // 设置勾选状态
@Override
public void onClick(View v) {
LogUtils.setTAGListEnable(item.getTag(), ((CheckBox)v).isChecked());
}
});
// 勾选框点击监听(更新 TAG 启用状态)
holder.tagCb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean isChecked = ((CheckBox) v).isChecked();
// 调用 LogUtils 更新该 TAG 的启用状态
LogUtils.setTagEnable(item.getTag(), isChecked);
// 同步更新本地模型状态(避免刷新后状态不一致)
item.setChecked(isChecked);
}
});
return convertView;
}
public class ViewHolder {
TextView tvText;
CheckBox cbChecked;
/**
* 列表项 ViewHolder缓存控件提升列表滑动性能
*/
private class ViewHolder {
TextView tagTv; // TAG 名称文本控件
CheckBox tagCb; // TAG 启用状态勾选框
}
}
class SortMapEntryByKeyString implements Comparator<TAGItemModel> {
private boolean mIsDesc = true;
// isDesc 是否降序排列
public SortMapEntryByKeyString(boolean isDesc) {
mIsDesc = isDesc;
// ====================== 内部类TAG 排序比较器(中文兼容) ======================
/**
* TAG 名称排序比较器(实现 Comparator
* 支持中文排序(基于系统默认中文 Locale可选择升序/降序
*/
private class TagAscComparator implements Comparator<TAGItemModel> {
/** 排序方向true升序false降序 */
private boolean isAsc;
/** 中文排序器(兼容中文汉字排序) */
private Collator chineseCollator = Collator.getInstance(java.util.Locale.CHINA);
public TagAscComparator(boolean isAsc) {
this.isAsc = isAsc;
}
Collator cmp = Collator.getInstance(java.util.Locale.CHINA);
/**
* 比较两个 TAGItemModel按 TAG 名称排序)
* @param o1 第一个比较对象
* @param o2 第二个比较对象
* @return 比较结果正数o1 在 o2 后负数o1 在 o2 前0相等
*/
@Override
public int compare(TAGItemModel o1, TAGItemModel o2) {
if (mIsDesc) {
return o1.getTag().compareTo(o2.getTag());
String tag1 = o1.getTag();
String tag2 = o2.getTag();
// 处理空值(空 TAG 排在最前)
if (tag1 == null) return -1;
if (tag2 == null) return 1;
// 根据排序方向返回比较结果
if (isAsc) {
return chineseCollator.compare(tag1, tag2); // 升序
} else {
return o2.getTag().compareTo(o1.getTag());
return chineseCollator.compare(tag2, tag1); // 降序
}
}
}
}

View File

@@ -121,18 +121,6 @@ public class ToastUtils {
LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置");
}
// ===================================== 新增isInited() 方法 =====================================
/**
* 判断 ToastUtils 是否已初始化(供外部调用,如 CrashHandleNotifyUtils 中的复制提示)
* @return true已初始化可正常显示吐司false未初始化/已释放(无法正常显示)
*/
public static boolean isInited() {
ToastUtils instance = getInstance();
// 双重校验1. 未释放 2. 上下文已设置(确保初始化完成)
return !instance.isReleased && instance.mContext != null;
}
// ===================================== 新增结束 =====================================
/**
* 外部接口:显示短时长吐司
* @param message 吐司内容
@@ -255,6 +243,7 @@ public class ToastUtils {
instance.mWorkerThread.join(1000);
} catch (InterruptedException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "线程退出异常", e);
Thread.currentThread().interrupt();
}
instance.mWorkerThread = null;

View File

@@ -1,151 +0,0 @@
package cc.winboll.studio.libappbase.activities;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/01 10:00
* @Describe 崩溃通知复制动作接收活动(透明无界面)
* 专门接收崩溃通知中“复制日志”按钮的点击事件,处理崩溃日志复制到剪贴板逻辑
* 优势:相比广播接收器,活动在应用崩溃后仍能被系统唤醒,确保复制功能稳定生效
*/
public class CrashCopyReceiverActivity extends Activity {
/** 日志 TAG标识当前类日志来源 */
public static final String TAG = "CrashCopyReceiverActivity";
/** 复制动作 Action需与 CrashHandleNotifyUtils 中一致) */
public static final String ACTION_COPY_CRASH_LOG = "cc.winboll.studio.action.COPY_CRASH_LOG";
/** 崩溃日志 Extra 键(需与 CrashHandleNotifyUtils 中一致) */
public static final String EXTRA_CRASH_LOG = "EXTRA_CRASH_LOG";
/** 复制成功提示文本 */
private static final String COPY_SUCCESS_TIP = "崩溃日志已复制到剪贴板";
/** 活动自动关闭延迟(毫秒):避免占用资源,复制完成后快速关闭 */
private static final long AUTO_FINISH_DELAY = 500;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 强制设置活动透明(兼容不同主题配置,避免出现白色界面)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
getWindow().setFlags(
android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
);
getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
}
// 处理复制按钮点击事件
handleCopyAction(getIntent());
}
/**
* 处理崩溃日志复制逻辑
* @param intent 接收的意图(携带崩溃日志)
*/
private void handleCopyAction(Intent intent) {
if (intent == null || !ACTION_COPY_CRASH_LOG.equals(intent.getAction())) {
LogUtils.e(TAG, "非崩溃日志复制意图,直接关闭活动");
finish();
return;
}
// 从意图中获取崩溃日志
String crashLog = intent.getStringExtra(EXTRA_CRASH_LOG);
if (crashLog == null || crashLog.isEmpty()) {
LogUtils.e(TAG, "崩溃日志为空,无法复制");
showTip("复制失败:崩溃日志为空");
finish();
return;
}
// 复制日志到剪贴板
if (copyTextToClipboard(crashLog)) {
LogUtils.d(TAG, "崩溃日志复制成功,长度:" + crashLog.length() + "字符");
showTip(COPY_SUCCESS_TIP);
} else {
LogUtils.e(TAG, "崩溃日志复制失败");
showTip("复制失败,请重试");
}
// 延迟关闭活动(确保提示能正常显示,用户有感知)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
finish(); // 关闭透明活动,释放资源
// 适配 Android 12+API 31+):避免活动留在任务栈中(修复 S 常量报错)
if (Build.VERSION.SDK_INT >= 31) { // 核心修复:用 31 替代 Build.VERSION_CODES.S
finishAndRemoveTask();
}
}
}, AUTO_FINISH_DELAY);
}
/**
* 复制文本到系统剪贴板兼容全Android版本
* @param text 需复制的文本(崩溃日志)
* @return true复制成功false复制失败
*/
private boolean copyTextToClipboard(String text) {
try {
// 适配 Android 11+API 30+)剪贴板 API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
android.content.ClipData clipData = android.content.ClipData.newPlainText("崩溃日志", text);
clipboard.setPrimaryClip(clipData);
} else {
// 适配 Android 10 及以下版本剪贴板 API
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboard.setText(text);
}
return true;
} catch (Exception e) {
LogUtils.e(TAG, "复制文本到剪贴板失败", e);
return false;
}
}
/**
* 显示提示信息(优先使用 ToastUtils失败则降级为系统 Toast
* @param tip 提示内容
*/
private void showTip(String tip) {
try {
// 优先使用项目封装的 ToastUtils确保样式统一
if (ToastUtils.isInited()) {
ToastUtils.show(tip);
} else {
// 降级使用系统 Toast确保提示能正常显示
Toast.makeText(this, tip, Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
LogUtils.e(TAG, "显示提示失败", e);
}
}
/**
* 处理重复意图(避免多次触发复制)
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent); // 更新当前意图
handleCopyAction(intent); // 重新处理复制动作
}
/**
* 禁止活动旋转时重建(避免复制逻辑重复执行)
*/
@Override
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
}

View File

@@ -1,286 +0,0 @@
package cc.winboll.studio.libappbase.utils;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity;
import cc.winboll.studio.libappbase.CrashHandler;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.R;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/29 21:12
* @Describe 应用崩溃处理通知实用工具集(类库专用,支持宿主应用唤醒活动)
* 核心功能应用崩溃时捕获错误日志发送通知到系统通知栏3行内容省略+复制按钮点击复制按钮唤醒CrashCopyReceiverActivity完成日志拷贝
*/
public class CrashHandleNotifyUtils {
public static final String TAG = "CrashHandleNotifyUtils";
/** 通知渠道IDAndroid 8.0+ 必须,用于归类通知) */
private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel";
/** 通知渠道名称(用户可见,描述渠道用途) */
private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知";
/** 通知ID唯一标识一条通知避免重复创建 */
private static final int CRASH_NOTIFY_ID = 0x001;
/** Android 12 对应 API 版本号31替代 Build.VERSION_CODES.S */
private static final int API_LEVEL_ANDROID_12 = 31;
/** PendingIntent.FLAG_IMMUTABLE 常量值API 31+),避免依赖高版本 SDK */
private static final int FLAG_IMMUTABLE = 0x00000040;
/** 通知内容最大行数控制在3行超出部分省略 */
private static final int NOTIFICATION_MAX_LINES = 3;
/** 复制按钮请求码(区分多个 PendingIntent */
private static final int REQUEST_CODE_COPY = 0x002;
/**
* 处理未捕获异常(核心方法,类库场景专用:强制使用宿主应用包名构建意图)
* 1. 提取应用名称和崩溃日志;
* 2. 创建并发送系统通知3行内容省略+复制按钮);
* 3. 兼容 Android 8.0+ 通知渠道机制,适配低版本系统。
* @param app 应用全局 Application 实例(宿主应用的 Application确保包名正确
* @param intent 存储崩溃信息的意图extra 中携带崩溃日志)
*/
public static void handleUncaughtException(Application app, Intent intent) {
// 1. 提取应用名称(优化:从宿主 Application 中获取真实应用名)
String appName = getAppName(app);
// 2. 提取崩溃日志(从 Intent Extra 中获取,对应 CrashHandler 存储的崩溃信息)
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
// 校验参数(避免空指针,确保通知正常发送)
if (app == null || appName == null || errorLog == null) {
LogUtils.e(TAG, "发送崩溃通知失败参数为空app=" + app + ", appName=" + appName + ", errorLog=" + errorLog + "");
return;
}
// 3. 发送崩溃通知到通知栏(类库场景:强制用宿主包名构建意图)
sendCrashNotification(app, appName, errorLog, app.getPackageName());
}
/**
* 获取应用真实名称(从宿主 AndroidManifest 中读取 android:label
* @param context 上下文(宿主 Application 实例,确保获取正确的应用名称)
* @return 应用名称(读取失败返回 "未知应用"
*/
private static String getAppName(Context context) {
try {
// 从宿主包管理器中获取应用信息(类库场景必须用宿主上下文)
return context.getPackageManager().getApplicationLabel(
context.getApplicationInfo()
).toString();
} catch (Exception e) {
LogUtils.e(TAG, "获取应用名称失败", e);
return "未知应用";
}
}
/**
* 发送崩溃通知到系统通知栏(类库专用:新增宿主包名参数,确保意图跳转正确)
* @param context 上下文(宿主 Application 实例)
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志)
* @param hostPackageName 宿主应用包名(关键:用于构建跨类库的活动意图)
*/
private static void sendCrashNotification(Context context, String title, String content, String hostPackageName) {
// 1. 获取通知管理器(系统服务,用于发送/管理通知)
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) {
LogUtils.e(TAG, "发送崩溃通知失败:获取 NotificationManager 为空");
return;
}
// 2. 适配 Android 8.0+API 26+):创建通知渠道(必须,否则通知不显示)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createCrashNotifyChannel(notificationManager);
}
// 3. 构建通知意图(类库场景:用宿主包名构建意图,确保跳转成功)
PendingIntent launchPendingIntent = getLaunchPendingIntent(context, hostPackageName); // 主界面跳转意图
PendingIntent copyPendingIntent = getCopyPendingIntent(context, content, hostPackageName); // 唤醒复制活动的意图
// 4. 构建通知实例核心修复3行内容省略+复制按钮)
Notification notification = buildNotification(context, title, content, launchPendingIntent, copyPendingIntent);
// 5. 发送通知指定通知ID重复发送同ID会覆盖原通知
notificationManager.notify(CRASH_NOTIFY_ID, notification);
LogUtils.d(TAG, "崩溃通知发送成功(类库场景):标题=" + title + ",宿主包名=" + hostPackageName);
}
/**
* 创建崩溃通知渠道Android 8.0+ 必需)
* @param notificationManager 通知管理器
*/
private static void createCrashNotifyChannel(NotificationManager notificationManager) {
// 仅 Android 8.0+ 执行(避免低版本报错)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 构建通知渠道指定ID、名称、重要性
android.app.NotificationChannel channel = new android.app.NotificationChannel(
CRASH_NOTIFY_CHANNEL_ID,
CRASH_NOTIFY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT // 重要性:默认(不会弹窗,有声音提示)
);
// 可选:设置渠道描述(用户在设置中可见)
channel.setDescription("用于显示应用崩溃信息,支持复制日志");
// 注册通知渠道到系统
notificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "崩溃通知渠道创建成功:" + CRASH_NOTIFY_CHANNEL_ID);
}
}
/**
* 构建通知点击跳转意图(跳转宿主应用主界面,类库场景专用)
* @param context 上下文(宿主 Application 实例)
* @param hostPackageName 宿主应用包名(关键:确保跳转宿主主界面)
* @return 主界面跳转 PendingIntent
*/
private static PendingIntent getLaunchPendingIntent(Context context, String hostPackageName) {
// 1. 获取宿主应用主界面 Intent强制用宿主包名获取避免类库包名干扰
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(hostPackageName);
if (launchIntent == null) {
// 异常处理:若主界面 Intent 为空,创建空意图(避免崩溃)
launchIntent = new Intent();
}
// 2. 构建 PendingIntent延迟执行的意图类库场景增加 FLAG_ONE_SHOT 确保单次有效)
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// 适配 Android 12+API 31+):添加 FLAG_IMMUTABLE 避免安全警告
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
flags |= FLAG_IMMUTABLE;
}
return PendingIntent.getActivity(
context,
0, // 请求码(可忽略)
launchIntent,
flags
);
}
/**
* 构建复制按钮意图(类库场景专用:用宿主包名唤醒活动,确保跳转成功)
* @param context 上下文(宿主 Application 实例)
* @param errorLog 崩溃日志(需要复制的内容)
* @param hostPackageName 宿主应用包名(关键:确保系统能找到类库中的活动)
* @return 唤醒复制活动的 PendingIntent
*/
private static PendingIntent getCopyPendingIntent(Context context, String errorLog, String hostPackageName) {
// 1. 构建唤醒 CrashCopyReceiverActivity 的显式意图(类库场景关键:指定宿主包名)
Intent copyIntent = new Intent(context, CrashCopyReceiverActivity.class);
// 强制设置意图的包名为宿主包名(解决类库与宿主包名不匹配问题)
copyIntent.setPackage(hostPackageName);
// 设置动作与Activity中匹配确保意图精准匹配
copyIntent.setAction(CrashCopyReceiverActivity.ACTION_COPY_CRASH_LOG);
// 携带完整崩溃日志键与Activity中一致
copyIntent.putExtra(CrashCopyReceiverActivity.EXTRA_CRASH_LOG, errorLog);
// 类库场景增强标志:确保活动在宿主应用中能被唤醒,且不重复创建
copyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_ACTIVITY_NO_HISTORY); // 不保留活动历史,避免残留
// 2. 构建 PendingIntent类库场景使用 FLAG_ONE_SHOT 避免重复触发)
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT;
// 适配 Android 12+API 31+):添加 FLAG_IMMUTABLE 避免安全警告
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
flags |= FLAG_IMMUTABLE;
}
// 3. 返回活动类型的 PendingIntent类库场景用宿主上下文构建确保权限通过
return PendingIntent.getActivity(
context,
REQUEST_CODE_COPY, // 唯一请求码,区分主界面意图
copyIntent,
flags
);
}
/**
* 构建通知实例核心修复3行内容省略+复制按钮)
* @param context 上下文(宿主 Application 实例)
* @param title 通知标题(应用名称)
* @param content 通知内容(崩溃日志)
* @param launchPendingIntent 通知点击跳转意图
* @param copyPendingIntent 唤醒复制活动的意图
* @return 构建完成的 Notification 对象
*/
private static Notification buildNotification(Context context, String title, String content, PendingIntent launchPendingIntent, PendingIntent copyPendingIntent) {
// 兼容 Android 8.0+指定通知渠道ID
Notification.Builder builder = new Notification.Builder(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
}
// 核心修复1用BigTextStyle控制“默认3行省略下拉显示完整”
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容"); // 底部省略提示
bigTextStyle.bigText(content); // 完整日志(下拉时显示)
bigTextStyle.setBigContentTitle(title); // 下拉后的标题(与主标题一致)
builder.setStyle(bigTextStyle);
// 核心修改2添加复制按钮Android 4.1+ 支持通知按钮,类库场景用系统图标避免资源缺失)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// 类库场景:用系统默认复制图标(避免自定义图标在宿主应用中缺失)
int copyIcon = R.drawable.ic_content_copy;
// 若类库有自定义图标,优先使用(需确保宿主应用能访问类库资源)
if (R.drawable.ic_content_copy != 0) {
copyIcon = R.drawable.ic_content_copy;
}
builder.addAction(
copyIcon, // 兼容图标(系统图标兜底)
"复制日志", // 按钮文本
copyPendingIntent // 按钮点击意图唤醒CrashCopyReceiverActivity
);
}
// 配置通知核心参数移除setLines避免报错
builder
.setSmallIcon(context.getApplicationInfo().icon) // 通知小图标(用宿主应用图标,避免类库图标缺失)
.setContentTitle(title) // 通知主标题(应用名称)
.setContentText(getShortContent(content)) // 核心3行内缩略文本
.setContentIntent(launchPendingIntent) // 通知主体点击跳转主界面
.setAutoCancel(true) // 点击通知后自动取消
.setWhen(System.currentTimeMillis()) // 通知创建时间
.setPriority(Notification.PRIORITY_DEFAULT); // 通知优先级
// 适配 Android 4.1+:确保文本显示正常,构建并返回通知
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return builder.build();
} else {
// Android 4.0 及以下版本,使用 getNotification() 方法
return builder.getNotification();
}
}
/**
* 辅助方法截取日志文本确保显示在3行内按字符数估算适配大多数设备
* @param content 完整崩溃日志
* @return 3行内的缩略文本
*/
private static String getShortContent(String content) {
if (content == null || content.isEmpty()) {
return "无崩溃日志";
}
// 估算3行字符数80字符可根据设备屏幕调整避免因字符过长导致换行超3行
int maxLength = 80;
if (content.length() <= maxLength) {
return content; // 不足3行直接返回完整文本
} else {
// 超出3行截取前80字符并加省略号确保视觉上仅显示3行
return content.substring(0, maxLength) + "...";
}
}
/**
* 释放资源(删除原广播注销逻辑,仅保留空实现便于兼容旧代码调用)
* @param context 上下文(宿主 Application 实例)
*/
public static void release(Context context) {
// 因已移除广播接收器,此处仅保留空实现,避免调用方报错
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(类库场景,无广播接收器需注销)");
}
}

View File

@@ -1,129 +0,0 @@
package cc.winboll.studio.libappbase.views;
/**
* @Author ZhanGSKen@AliYun.Com
* @Date 2025/03/12 12:29:01
* @Describe 水平布局的 ListView
*/
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ListView;
import android.widget.Scroller;
import cc.winboll.studio.libappbase.LogUtils;
public class HorizontalListView extends ListView {
public static final String TAG = "HorizontalListView";
private int verticalOffset = 0;
private Scroller scroller;
private int totalWidth;
public HorizontalListView(Context context) {
super(context);
init();
}
public HorizontalListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
scroller = new Scroller(getContext());
setHorizontalScrollBarEnabled(true);
setVerticalScrollBarEnabled(false);
}
public void setVerticalOffset(int verticalOffset) {
this.verticalOffset = verticalOffset;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
int childCount = getChildCount();
int left = getPaddingLeft();
int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
totalWidth = left;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
child.layout(left, verticalOffset, left + width, verticalOffset + height);
left += width;
}
totalWidth = left + getPaddingRight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int x, int y) {
int dx = x - getScrollX();
int dy = y - getScrollY();
scroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300); // 300ms平滑动画
invalidate();
}
@Override
public int computeHorizontalScrollRange() {
return totalWidth;
}
@Override
public int computeHorizontalScrollOffset() {
return getScrollX();
}
@Override
public int computeHorizontalScrollExtent() {
return getWidth();
}
public void scrollToItem(int position) {
if (position < 0 || position >= getChildCount()) {
LogUtils.d(TAG, "无效的position: " + position);
return;
}
View targetView = getChildAt(position);
int targetLeft = targetView.getLeft();
int scrollX = targetLeft - getPaddingLeft();
// 修正最大滚动范围计算
int maxScrollX = totalWidth;
scrollX = Math.max(0, Math.min(scrollX, maxScrollX));
// 强制重新布局和绘制
requestLayout();
invalidateViews();
smoothScrollTo(scrollX, 0);
LogUtils.d(TAG, String.format("滚动到position: %d, scrollX: %d computeHorizontalScrollRange() %d", position, scrollX, computeHorizontalScrollRange()));
}
public void resetScrollToStart() {
// 强制重新布局和绘制
requestLayout();
invalidateViews();
smoothScrollTo(0, 0);
}
}

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#ff000000"
android:pathData="M19,21H8V7H19M19,5H8A2,2 0,0 0,6 7V21A2,2 0,0 0,8 23H19A2,2 0,0 0,21 21V7A2,2 0,0 0,19 5M16,1H4A2,2 0,0 0,2 3V17H4V3H16V1Z"/>
</vector>

View File

@@ -99,7 +99,7 @@
android:layout_weight="1.0"
android:id="@+id/viewlogHorizontalScrollView1">
<cc.winboll.studio.libappbase.views.HorizontalListView
<cc.winboll.studio.libappbase.HorizontalListView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/tags_listview"/>

View File

@@ -21,7 +21,7 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
api 'cc.winboll.studio:libappbase:15.10.9'
api 'cc.winboll.studio:libappbase:15.9.5'
// 二维码类库
api 'com.google.zxing:core:3.4.1'
@@ -32,8 +32,6 @@ dependencies {
// Html 解析
api 'org.jsoup:jsoup:1.13.1'
api 'com.google.code.gson:gson:2.10.1'
// SSH
//api 'com.jcraft:jsch:0.1.55'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Sep 29 01:15:55 HKT 2025
stageCount=3
#Mon Sep 01 07:56:11 HKT 2025
stageCount=7
libraryProject=libapputils
baseVersion=15.10
publishVersion=15.10.2
baseVersion=15.8
publishVersion=15.8.6
buildCount=0
baseBetaVersion=15.10.3
baseBetaVersion=15.8.7

View File

@@ -1,29 +0,0 @@
package cc.winboll.studio.libapputils.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/02/15 20:05:03
* @Describe AppUtils
*/
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import cc.winboll.studio.libappbase.LogUtils;
public class AppUtils {
public static final String TAG = "AppUtils";
public static String getAppNameByPackageName(Context context, String packageName) {
PackageManager packageManager = context.getPackageManager();
try {
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0);
return (String) packageManager.getApplicationLabel(applicationInfo);
} catch (NameNotFoundException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
return "";
}
}
}

View File

@@ -9,16 +9,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.net.Uri;
import android.support.v4.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -28,6 +22,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import android.support.v4.content.FileProvider;
public class FileUtils {
@@ -102,6 +97,36 @@ public class FileUtils {
}
}
//
// 把字符串写入文件,指定 UTF-8 编码
//
public static void writeStringToFile(String szFilePath, String szContent) throws IOException {
File file = new File(szFilePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
FileOutputStream outputStream = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
writer.write(szContent);
writer.close();
}
//
// 读取文件到字符串,指定 UTF-8 编码
//
public static String readStringFromFile(String szFilePath) throws IOException {
File file = new File(szFilePath);
FileInputStream inputStream = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
StringBuilder content = new StringBuilder();
int character;
while ((character = reader.read()) != -1) {
content.append((char) character);
}
reader.close();
return content.toString();
}
public static boolean copyFile(File srcFile, File dstFile) {
if (!srcFile.exists()) {
LogUtils.d(TAG, "The original file does not exist.");
@@ -129,113 +154,4 @@ public class FileUtils {
}
return false;
}
/**
* 读取文件为字节数组Java 7 语法)
*/
public static byte[] readByteArrayFromFile(String filePath) {
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
fis = new FileInputStream(filePath);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
// 手动关闭流Java 7 不支持 try-with-resources
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 写入字节数组到文件Java 7 语法)
*/
public static boolean writeByteArrayToFile(byte[] data, String filePath) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(filePath);
fos.write(data);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static String readStringFromFile(String filePath) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(filePath));
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append(System.getProperty("line.separator"));
}
// 去除最后一个换行符(可选)
if (content.length() > 0) {
content.deleteCharAt(content.length() - 1);
}
return content.toString();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static boolean writeStringToFile(String content, String filePath, boolean append) {
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(filePath, append));
writer.write(content);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

View File

@@ -33,7 +33,7 @@ android {
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.11"
versionName "15.12"
if(true) {
versionName = genVersionName("${versionName}")
}
@@ -56,7 +56,12 @@ dependencies {
api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// uCrop 核心依赖(最新稳定版)
implementation 'com.github.yalantis:ucrop:2.2.8'
// 兼容AndroidX若项目用AndroidX必须添加
//implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
// 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0'
// SSH
@@ -77,8 +82,8 @@ dependencies {
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'
implementation 'cc.winboll.studio:libaes:15.11.6'
implementation 'cc.winboll.studio:libappbase:15.11.0'
implementation 'cc.winboll.studio:libaes:15.11.8'
implementation 'cc.winboll.studio:libappbase:15.11.6'
//api fileTree(dir: 'libs', include: ['*.aar'])
api fileTree(dir: 'libs', include: ['*.jar'])

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Nov 26 16:27:33 HKT 2025
stageCount=9
#Thu Dec 04 10:29:58 GMT 2025
stageCount=1
libraryProject=
baseVersion=15.11
publishVersion=15.11.8
buildCount=0
baseBetaVersion=15.11.9
baseVersion=15.12
publishVersion=15.12.0
buildCount=6
baseBetaVersion=15.12.1

View File

@@ -150,7 +150,7 @@
</activity>
<activity
android:name="cc.winboll.studio.powerbell.activities.BackgroundPictureActivity"
android:name="cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true"
android:launchMode="singleTask">
@@ -230,6 +230,15 @@
<activity android:name="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.SettingsActivity"/>
<!-- 1. 注册 UCropActivity关键解决崩溃 -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"> <!-- 必须添加Android 12+ 要求显式声明 exported -->
</activity>
</application>
</manifest>
</manifest>

View File

@@ -2,7 +2,6 @@ package cc.winboll.studio.powerbell;
import android.content.Context;
import android.os.Environment;
import android.view.Gravity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
@@ -12,7 +11,7 @@ import java.io.File;
public class App extends GlobalApplication {
public static final String TAG = "GlobalApplication";
public static final String TAG = "App";
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1";
@@ -34,6 +33,7 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
//setIsDebugging(false);
setIsDebugging(BuildConfig.DEBUG);
// 临时文件夹方案1

View File

@@ -16,15 +16,18 @@ import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.activities.AboutActivity;
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.BatteryReportActivity;
import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
import cc.winboll.studio.powerbell.activities.SettingsActivity;
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
/**
* 主活动类修复小米广告SDK空Context崩溃问题
@@ -109,6 +112,8 @@ public class MainActivity extends WinBoLLActivity {
tx.commit();
}
showFragment(mMainViewFragment);
PermissionUtils.getInstance().checkAndRequestStoragePermission(this);
}
@Override
@@ -198,7 +203,7 @@ public class MainActivity extends WinBoLLActivity {
reloadBackground();
setBackgroundColor();
if (mADsBannerView != null) {
mADsBannerView.resumeADs();
mADsBannerView.resumeADs(MainActivity.this);
}
// // 修复:优化广告请求逻辑(添加生命周期判断 + 主线程执行)
@@ -234,14 +239,16 @@ public class MainActivity extends WinBoLLActivity {
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
int menuItemId = item.getItemId();
if (menuItemId == R.id.action_about) {
if (menuItemId == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
} else if (menuItemId == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
} else if (menuItemId == R.id.action_battery_report) {
startActivity(new Intent(this, BatteryReportActivity.class));
} else if (menuItemId == R.id.action_clearrecord) {
startActivity(new Intent(this, ClearRecordActivity.class));
} else if (menuItemId == R.id.action_changepicture) {
startActivity(new Intent(this, BackgroundPictureActivity.class));
startActivity(new Intent(this, BackgroundSettingsActivity.class));
} else if (menuItemId == R.id.action_log) {
LogActivity.startLogActivity(this);
} else if (menuItemId == R.id.action_unittestactivity) {
@@ -278,8 +285,8 @@ public class MainActivity extends WinBoLLActivity {
if (isFinishing() || isDestroyed()) {
return;
}
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this);
BackgroundBean bean = utils.getCurrentBackgroundBean();
int nPixelColor = bean.getPixelColor();
RelativeLayout mainLayout = findViewById(R.id.activitymainRelativeLayout1);
if (mainLayout != null) {

View File

@@ -1,659 +0,0 @@
package cc.winboll.studio.powerbell.activities;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.view.View;
import android.widget.RelativeLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libaes.views.AToolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog;
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.UriUtil;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class BackgroundPictureActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
public static final String TAG = "BackgroundPictureActivity";
public BackgroundPictureUtils mBackgroundPictureUtils;
// 图片选择请求码
public static final int REQUEST_SELECT_PICTURE = 0;
public static final int REQUEST_TAKE_PHOTO = 1;
public static final int REQUEST_CROP_IMAGE = 2;
private static final int STORAGE_PERMISSION_REQUEST = 100;
private AToolbar mAToolbar;
private File mfBackgroundDir; // 背景图片存储文件夹
private File mfPictureDir; // 拍照与剪裁临时文件夹
private File mfTakePhoto; // 拍照文件
private File mfRecivedPicture; // 接收的图片文件
private File mfTempCropPicture; // 剪裁临时文件
private File mfRecivedCropPicture; // 剪裁后的目标文件
private String preViewFileBackgroundView = "";
BackgroundView bvPreviewBackground;
boolean isCommitSettings = false;
// 静态变量
public static String _mszRecivedCropPicture = "RecivedCrop.jpg";
private static String _mszCommonFileType = "jpeg";
private int mnPictureCompress = 100;
private static String _RecivedPictureFileName;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_backgroundpicture);
initEnv();
// 初始化工具类和文件夹
mBackgroundPictureUtils = BackgroundPictureUtils.getInstance(this);
mfBackgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir());
if (!mfBackgroundDir.exists()) {
mfBackgroundDir.mkdirs();
}
mfPictureDir = new File(App.getTempDirPath());
if (!mfPictureDir.exists()) {
mfPictureDir.mkdirs();
}
// 初始化文件对象
mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg");
mfTempCropPicture = new File(mfPictureDir, "TempCrop.jpg");
mfRecivedPicture = getRecivedPictureFile(this);
mfRecivedCropPicture = new File(mfBackgroundDir, _mszRecivedCropPicture);
// 初始化工具栏
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
setActionBar(mAToolbar);
mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture);
getActionBar().setDisplayHomeAsUpEnabled(true);
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish(); // 点击导航栏返回按钮,触发 finish()
}
});
// 设置按钮点击事件
findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener);
findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener);
findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener);
findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener);
updatePreviewBackground();
// 处理分享的图片
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this);
dlg.show();
}
}
private void initEnv() {
LogUtils.d(TAG, "initEnv()");
_RecivedPictureFileName = "Recived.data";
}
public static String getBackgroundFileName() {
return _mszRecivedCropPicture;
}
@Override
public void onAcceptRecivedPicture(String szPreRecivedPictureName) {
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this);
utils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
utils.saveData();
File sourceFile = new File(utils.getBackgroundDir(), szPreRecivedPictureName);
if (FileUtils.copyFile(sourceFile, mfRecivedPicture)) {
startCropImageActivity(false);
} else {
ToastUtils.show("图片复制失败,请重试");
}
}
/**
* 更新背景图片预览
*/
public void updatePreviewBackground() {
LogUtils.d(TAG, "updatePreviewBackground");
//ImageView ivPreviewBackground = (ImageView) findViewById(R.id.activitybackgroundpictureImageView1);
bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1);
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this);
utils.loadBackgroundPictureBean();
boolean isUseBackgroundFile = utils.getBackgroundPictureBean().isUseBackgroundFile();
if (isUseBackgroundFile && mfRecivedCropPicture.exists()) {
//try {
String filePath = utils.getBackgroundDir() + getBackgroundFileName();
preViewFileBackgroundView = filePath;
bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView);
/*Drawable drawable = FileUtils.getImageDrawable(filePath);
if (drawable != null) {
//drawable.setAlpha(120);
//bvPreviewBackground.setImageDrawable(drawable);
}*/
//ToastUtils.show("背景图片已更新");
// } catch (IOException e) {
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// ToastUtils.show("背景图片加载失败");
// }
} else {
ToastUtils.show("未使用背景图片");
preViewFileBackgroundView = "";
bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView);
// Drawable drawable = getResources().getDrawable(R.drawable.blank10x10);
// if (drawable != null) {
// drawable.setAlpha(120);
// bvPreviewBackground.setImageDrawable(drawable);
// }
}
}
// 点击事件监听器
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
bean.setIsUseBackgroundFile(false);
utils.saveData();
updatePreviewBackground();
}
};
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (checkAndRequestStoragePermission()) {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_SELECT_PICTURE);
}
}
};
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
File fCheck = new File(mfBackgroundDir, getBackgroundFileName());
if (fCheck.exists()) {
startCropImageActivity(false);
} else {
ToastUtils.show("没有可剪裁的图片");
}
}
};
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
File fCheck = new File(mfBackgroundDir, getBackgroundFileName());
if (fCheck.exists()) {
startCropImageActivity(true);
} else {
ToastUtils.show("没有可剪裁的图片");
}
}
};
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "onTakePhotoClickListener");
LogUtils.d(TAG, "mfTakePhoto : " + mfTakePhoto.getPath());
if (mfTakePhoto.exists()) {
mfTakePhoto.delete();
}
try {
mfTakePhoto.createNewFile();
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
ToastUtils.show("拍照文件创建失败");
return;
}
if (checkAndRequestStoragePermission()) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
}
}
};
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
utils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
utils.saveData();
updatePreviewBackground();
}
};
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// 从文件路径启动像素拾取活动
//String imagePath = "/storage/emulated/0/DCIM/Camera/sample.jpg";
String imagePath = mfRecivedCropPicture.toString();
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
intent.putExtra("imagePath", imagePath);
startActivity(intent);
//App.getWinBoLLActivityManager().startWinBoLLActivity(getActivity(), intent, PixelPickerActivity.class);
}
};
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
bean.setPixelColor(0);
utils.saveData();
setBackgroundColor();
}
};
/**
* 压缩图片并保存到接收文件
*/
void compressQualityToRecivedPicture(Bitmap bitmap) {
OutputStream outStream = null;
try {
mfRecivedPicture = getRecivedPictureFile(this);
if (!mfRecivedPicture.exists()) {
mfRecivedPicture.createNewFile();
}
FileOutputStream fos = new FileOutputStream(mfRecivedPicture);
outStream = new BufferedOutputStream(fos);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
outStream.flush();
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
ToastUtils.show("图片压缩失败");
} finally {
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
/**
* 启动图片裁剪活动
* @param isCropFree 是否自由裁剪
*/
public void startCropImageActivity(boolean isCropFree) {
LogUtils.d(TAG, "startCropImageActivity");
BackgroundPictureBean bean = mBackgroundPictureUtils.loadBackgroundPictureBean();
mfRecivedPicture = getRecivedPictureFile(this);
Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture);
LogUtils.d(TAG, "uri : " + uri.toString());
if (mfTempCropPicture.exists()) {
mfTempCropPicture.delete();
}
try {
mfTempCropPicture.createNewFile();
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
ToastUtils.show("剪裁临时文件创建失败");
return;
}
Uri cropOutPutUri = Uri.fromFile(mfTempCropPicture);
LogUtils.d(TAG, "mfTempCropPicture : " + mfTempCropPicture.getPath());
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/" + _mszCommonFileType);
intent.putExtra("crop", "true");
intent.putExtra("noFaceDetection", true);
if (!isCropFree) {
intent.putExtra("aspectX", bean.getBackgroundWidth());
intent.putExtra("aspectY", bean.getBackgroundHeight());
}
intent.putExtra("return-data", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri);
intent.putExtra("scale", true);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, REQUEST_CROP_IMAGE);
}
/**
* 保存剪裁后的Bitmap优化版
*/
private void saveCropBitmap(Bitmap bitmap) {
if (bitmap == null) {
ToastUtils.show("剪裁图片为空");
return;
}
// 内存优化:大图片自动缩放
Bitmap scaledBitmap = bitmap;
if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB
float scale = 1.0f;
while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) {
scale -= 0.2f; // 每次缩小20%
if (scale < 0.2f) break; // 最小缩放到20%
scaledBitmap = scaleBitmap(scaledBitmap, scale);
}
if (scaledBitmap != bitmap) {
bitmap.recycle(); // 回收原Bitmap
}
}
// 优化:创建保存目录
File backgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir());
if (!backgroundDir.exists()) {
if (!backgroundDir.mkdirs()) {
ToastUtils.show("无法创建保存目录");
if (scaledBitmap != bitmap) scaledBitmap.recycle();
return;
}
}
File saveFile = new File(backgroundDir, getBackgroundFileName());
// 优化:检查文件是否可写
if (saveFile.exists() && !saveFile.canWrite()) {
if (!saveFile.delete()) {
ToastUtils.show("无法删除旧文件");
if (scaledBitmap != bitmap) scaledBitmap.recycle();
return;
}
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(saveFile);
boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
fos.flush();
if (success) {
ToastUtils.show("保存成功");
// 更新数据
mBackgroundPictureUtils.getBackgroundPictureBean().setIsUseBackgroundFile(true);
updatePreviewBackground();
} else {
ToastUtils.show("图片压缩保存失败");
}
} catch (FileNotFoundException e) {
LogUtils.e(TAG, "文件未找到" + e);
ToastUtils.show("保存失败:文件路径错误");
} catch (IOException e) {
LogUtils.e(TAG, "写入异常" + e);
ToastUtils.show("保存失败:磁盘可能已满或路径错误");
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "流关闭异常" + e);
}
}
if (scaledBitmap != null && !scaledBitmap.isRecycled()) {
scaledBitmap.recycle();
}
}
}
/**
* 缩放Bitmap
*/
private Bitmap scaleBitmap(Bitmap original, float scale) {
if (original == null) {
return null;
}
int width = (int) (original.getWidth() * scale);
int height = (int) (original.getHeight() * scale);
return Bitmap.createScaledBitmap(original, width, height, true);
}
/**
* 分享图片
*/
void sharePicture() {
Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture);
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
shareIntent.setType("image/" + _mszCommonFileType);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent, "Share Image"));
}
public static File getRecivedPictureFile(Context context) {
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(context);
utils.loadBackgroundPictureBean();
return new File(utils.getBackgroundDir(), _RecivedPictureFileName);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_SELECT_PICTURE && resultCode == RESULT_OK) {
try {
Uri selectedImage = data.getData();
LogUtils.d(TAG, "Uri is : " + selectedImage.toString());
File fSrcImage = new File(UriUtil.getFilePathFromUri(this, selectedImage));
mfRecivedPicture = getRecivedPictureFile(this);
if (FileUtils.copyFile(fSrcImage, mfRecivedPicture)) {
startCropImageActivity(false);
} else {
ToastUtils.show("图片复制失败,请重试");
}
} catch (Exception e) {
LogUtils.e(TAG, "选择图片异常" + e);
ToastUtils.show("选择图片失败:" + e.getMessage());
}
} else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) {
LogUtils.d(TAG, "REQUEST_TAKE_PHOTO");
Bundle extras = data.getExtras();
if (extras != null) {
Bitmap imageBitmap = (Bitmap) extras.get("data");
if (imageBitmap != null) {
compressQualityToRecivedPicture(imageBitmap);
startCropImageActivity(false);
} else {
ToastUtils.show("拍照图片为空");
}
} else {
ToastUtils.show("拍照数据获取失败");
}
} else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) {
LogUtils.d(TAG, "CROP_IMAGE_REQUEST_CODE");
try {
Bitmap cropBitmap = null;
// 方案1通过Intent获取剪裁后的Bitmap
if (data != null && data.hasExtra("data")) {
cropBitmap = data.getParcelableExtra("data");
} else if (mfTempCropPicture.exists()) {
cropBitmap = BitmapFactory.decodeFile(mfTempCropPicture.getPath());
} else {
ToastUtils.show("剪裁文件不存在");
return;
}
if (cropBitmap != null) {
saveCropBitmap(cropBitmap);
} else {
ToastUtils.show("获取剪裁图片失败");
}
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "内存溢出" + e);
ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片");
} catch (Exception e) {
LogUtils.e(TAG, "剪裁保存异常" + e);
ToastUtils.show("保存失败:" + e.getMessage());
}/* finally {
// 安全删除临时文件
if (mfTempCropPicture.exists()) {
mfTempCropPicture.delete();
}
}*/
} else if (resultCode != RESULT_OK) {
LogUtils.d(TAG, "操作取消或失败requestCode: " + requestCode);
ToastUtils.show("操作已取消");
}
}
/**
* 检查类型是否为图片
*/
private boolean isImageType(String type) {
return type.startsWith("image/") || "image/jpeg".equals(type) ||
"image/jpg".equals(type) || "image/png".equals(type) ||
"image/webp".equals(type);
}
/**
* 检查并申请存储权限
*/
private boolean checkAndRequestStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
STORAGE_PERMISSION_REQUEST);
return false;
}
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == STORAGE_PERMISSION_REQUEST) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
ToastUtils.show("存储权限已获取");
} else {
ToastUtils.show("需要存储权限才能保存图片");
}
}
}
void setBackgroundColor() {
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
int nPixelColor = bean.getPixelColor();
RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1);
mainLayout.setBackgroundColor(nPixelColor);
}
@Override
protected void onResume() {
super.onResume();
setBackgroundColor();
}
public void onNetworkBackgroundDialog(View view) {
// 在需要显示对话框的地方(如网络状态监听回调中)
NetworkBackgroundDialog dialog = new NetworkBackgroundDialog(this, new NetworkBackgroundDialog.OnDialogClickListener() {
@Override
public void onConfirm() {
ToastUtils.show("onConfirm");
// 处理确认逻辑(如允许后台网络使用)
LogUtils.d("MainActivity", "用户允许后台网络使用");
// 执行具体业务:如开启后台网络请求服务
}
@Override
public void onCancel() {
ToastUtils.show("onCancel");
// 处理取消逻辑(如禁止后台网络使用)
LogUtils.d("MainActivity", "用户禁止后台网络使用");
// 执行具体业务:如关闭后台网络请求
}
});
// 可选:修改对话框标题和内容(适配自定义场景)
dialog.setTitle("网络图片下载对话框");
dialog.setContent("是否下载地址中的图片资源,作为应用背景图片?");
// 显示对话框
dialog.show();
}
/**
* 重写finish方法确保所有退出场景都触发Toast
*/
@Override
public void finish() {
if (!isCommitSettings) {
YesNoAlertDialog.show(this, "应用背景更改提示:", "是否应用预览图片?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onNo() {
isCommitSettings = true;
finish();
}
@Override
public void onYes() {
bvPreviewBackground.saveToBackgroundSources(preViewFileBackgroundView);
isCommitSettings = true;
finish();
}
});
} else {
super.finish();
}
}
}

View File

@@ -0,0 +1,771 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libaes.views.AToolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
public static final String TAG = "BackgroundSettingsActivity";
private BackgroundSourceUtils mBgSourceUtils;
private PermissionUtils mPermissionUtils;
public static final int REQUEST_SELECT_PICTURE = 0;
public static final int REQUEST_TAKE_PHOTO = 1;
public static final int REQUEST_CROP_IMAGE = 2;
private AToolbar mAToolbar;
private BackgroundView mBackgroundView;
private File mfTakePhoto;
volatile boolean isCommitSettings = false;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_background_settings);
mBackgroundView = (BackgroundView) findViewById(R.id.background_view);
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mPermissionUtils = PermissionUtils.getInstance();
File tempDir = new File(App.getTempDirPath());
if (!tempDir.exists()) {
tempDir.mkdirs();
}
mfTakePhoto = new File(tempDir, "TakePhoto.jpg");
File selectTempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp");
if (!selectTempDir.exists()) {
selectTempDir.mkdirs();
LogUtils.d(TAG, "【选图初始化】选图临时目录创建完成:" + selectTempDir.getAbsolutePath());
}
initToolbar();
initClickListeners();
initBackgroundViewByPreviewBean();
handleShareIntent();
LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成");
}
private void initBackgroundViewByPreviewBean() {
LogUtils.d(TAG, "【Bean初始化】正式Bean → 预览Bean");
mBgSourceUtils.setCurrentSourceToPreview();
doubleRefreshPreview();
LogUtils.d(TAG, "【Bean初始化】预览Bean初始化完成");
}
private void initToolbar() {
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
setActionBar(mAToolbar);
mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture);
getActionBar().setDisplayHomeAsUpEnabled(true);
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
}
private void initClickListeners() {
findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener);
findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener);
findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener);
findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener);
}
private void handleShareIntent() {
Intent intent = getIntent();
if (intent != null) {
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this);
dlg.show();
LogUtils.d(TAG, "【分享处理】收到分享图片意图");
}
}
}
boolean isImageType(String lowerMimeType) {
return lowerMimeType.equals("image/jpeg")
|| lowerMimeType.equals("image/png")
|| lowerMimeType.equals("image/tiff")
|| lowerMimeType.equals("image/jpg")
|| lowerMimeType.equals("image/svg+xml");
}
@Override
public void onAcceptRecivedPicture(String szPreRecivedPictureName) {
ToastUtils.show("图片接收功能暂未实现");
LogUtils.d(TAG, "【分享接收】图片名:" + szPreRecivedPictureName);
}
private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】取消背景图片");
BackgroundBean previewBackgroundBean = mBgSourceUtils.getPreviewBackgroundBean();
previewBackgroundBean.setIsUseBackgroundFile(false);
doubleRefreshPreview();
}
};
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】选择图片");
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
LogUtils.d(TAG, "【选图权限】已获取");
Intent[] intents = new Intent[3];
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
getContentIntent.setType("image/*");
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intents[0] = getContentIntent;
Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
pickIntent.setType("image/*");
pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intents[1] = pickIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
openDocIntent.setType("image/*");
openDocIntent.addCategory(Intent.CATEGORY_OPENABLE);
openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intents[2] = openDocIntent;
}
Intent validIntent = null;
for (int i = 0; i < intents.length; i++) {
Intent intent = intents[i];
if (intent != null && intent.resolveActivity(getPackageManager()) != null) {
validIntent = intent;
break;
}
}
if (validIntent != null) {
Intent chooser = Intent.createChooser(validIntent, "选择图片");
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
LogUtils.d(TAG, "【选图意图】启动图片选择");
} else {
LogUtils.d(TAG, "【选图意图】无相册应用");
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtils.show("未找到相册应用,请安装后重试");
new AlertDialog.Builder(BackgroundSettingsActivity.this)
.setTitle("无图片选择应用")
.setMessage("需要安装相册应用才能选择图片")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d"));
if (marketIntent.resolveActivity(getPackageManager()) != null) {
startActivity(marketIntent);
} else {
ToastUtils.show("无法打开应用商店");
}
}
})
.setNegativeButton("取消", null)
.show();
}
});
}
} else {
LogUtils.d(TAG, "【选图权限】已申请");
}
}
};
private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】固定比例裁剪");
// 调用裁剪工具类:传入上下文、预览图、固定比例(按视图宽高)、请求码
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
mBgSourceUtils.getPreviewBackgroundBean(),
mBackgroundView.getWidth(),
mBackgroundView.getHeight(),
false,
REQUEST_CROP_IMAGE
);
}
};
private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】自由裁剪");
// 调用裁剪工具类传入上下文、预览图、自由裁剪比例参数传0、请求码
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
mBgSourceUtils.getPreviewBackgroundBean(),
0,
0,
true,
REQUEST_CROP_IMAGE
);
}
};
private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】拍照");
if (mfTakePhoto.exists()) {
boolean deleteSuccess = mfTakePhoto.delete();
LogUtils.d(TAG, "【拍照准备】清理旧文件:" + (deleteSuccess ? "成功" : "失败"));
}
try {
boolean createSuccess = mfTakePhoto.createNewFile();
LogUtils.d(TAG, "【拍照准备】创建新文件:" + (createSuccess ? "成功" : "失败"));
if (!createSuccess) {
ToastUtils.show("拍照文件创建失败");
return;
}
} catch (IOException e) {
LogUtils.e(TAG, "【拍照异常】" + e.getMessage());
ToastUtils.show("拍照文件创建失败");
return;
}
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
LogUtils.d(TAG, "【拍照权限】已获取");
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
try {
Uri photoUri = getFileProviderUri(mfTakePhoto);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
LogUtils.d(TAG, "【拍照启动】Uri" + photoUri.toString());
} catch (Exception e) {
String errMsg = "拍照启动异常:" + e.getMessage();
ToastUtils.show(errMsg.substring(0, 20));
LogUtils.e(TAG, "【拍照失败】");
}
} else {
LogUtils.d(TAG, "【拍照权限】已申请");
}
}
};
private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
ToastUtils.show("图片接收功能暂未实现");
LogUtils.d(TAG, "【按钮点击】图片接收");
}
};
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】像素拾取");
String targetImagePath = mBgSourceUtils.getCurrentBackgroundFilePath();
File targetFile = new File(targetImagePath);
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
ToastUtils.show("无有效图片可拾取像素");
LogUtils.e(TAG, "【像素拾取失败】");
return;
}
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
intent.putExtra("imagePath", targetImagePath);
startActivity(intent);
LogUtils.d(TAG, "【像素拾取启动】");
}
};
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】清空像素颜色");
BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean();
int oldColor = bean.getPixelColor();
bean.setPixelColor(0);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
ToastUtils.show("像素颜色已清空");
LogUtils.d(TAG, "【像素清空】旧颜色:" + oldColor);
}
};
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "【回调触发】requestCode" + requestCode + "resultCode" + resultCode);
try {
if (requestCode == PermissionUtils.REQUEST_MANAGE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
handleStoragePermissionCallback();
return;
}
if (resultCode != RESULT_OK) {
handleOperationCancelOrFail();
return;
}
switch (requestCode) {
case REQUEST_SELECT_PICTURE:
handleSelectPictureResult(resultCode, data);
break;
case REQUEST_TAKE_PHOTO:
handleTakePhotoResult(resultCode, data);
break;
case REQUEST_CROP_IMAGE:
handleCropImageResult(requestCode, resultCode, data);
break;
default:
LogUtils.d(TAG, "【回调忽略】未知requestCode");
break;
}
} catch (Exception e) {
LogUtils.e(TAG, "【回调异常】" + e.getMessage());
ToastUtils.show("操作失败");
}
}
private void handleStoragePermissionCallback() {
if (Environment.isExternalStorageManager()) {
LogUtils.d(TAG, "【权限回调】已授予");
ToastUtils.show("存储权限已获取");
} else {
LogUtils.d(TAG, "【权限回调】已拒绝");
ToastUtils.show("存储权限不足");
}
}
private void handleTakePhotoResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK || data == null) {
handleOperationCancelOrFail();
return;
}
if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) {
ToastUtils.show("拍照文件无效");
return;
}
Bitmap photoBitmap = getTakePhotoBitmap(data);
if (photoBitmap != null && !photoBitmap.isRecycled()) {
mBgSourceUtils.compressQualityToRecivedPicture(photoBitmap);
} else {
ToastUtils.show("拍照图片为空");
return;
}
mBgSourceUtils.saveFileToPreviewBean(mfTakePhoto, mfTakePhoto.getAbsolutePath());
doubleRefreshPreview();
// 拍照后启动固定比例裁剪(调用工具类)
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
mBgSourceUtils.getPreviewBackgroundBean(),
mBackgroundView.getWidth(),
mBackgroundView.getHeight(),
false,
REQUEST_CROP_IMAGE
);
LogUtils.d(TAG, "【拍照完成】已启动裁剪");
}
private Bitmap getTakePhotoBitmap(Intent data) {
LogUtils.d(TAG, "【拍照Bitmap解析】开始");
if (mfTakePhoto != null && mfTakePhoto.exists()) {
LogUtils.d(TAG, "【拍照Bitmap解析】从文件解析");
Bitmap photoBitmap = parseCropTempFileToBitmap(mfTakePhoto);
if (photoBitmap != null && !photoBitmap.isRecycled()) {
LogUtils.d(TAG, "【拍照Bitmap解析】成功");
return photoBitmap;
} else {
LogUtils.w(TAG, "【拍照Bitmap解析】文件解析失败尝试Intent");
}
} else {
LogUtils.w(TAG, "【拍照Bitmap解析】文件无效尝试Intent");
}
if (data != null) {
try {
Bitmap thumbnailBitmap = (Bitmap) data.getParcelableExtra("data");
if (thumbnailBitmap != null && !thumbnailBitmap.isRecycled()) {
LogUtils.d(TAG, "【拍照Bitmap解析】从Intent获取成功");
return thumbnailBitmap;
} else {
LogUtils.e(TAG, "【拍照Bitmap解析】Intent解析失败");
}
} catch (Exception e) {
LogUtils.e(TAG, "【拍照Bitmap解析】Intent异常" + e.getMessage());
}
} else {
LogUtils.e(TAG, "【拍照Bitmap解析】Intent为空");
}
LogUtils.e(TAG, "【拍照Bitmap解析】失败");
ToastUtils.show("拍照图片解析失败");
return null;
}
private void handleSelectPictureResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK || data == null) {
handleOperationCancelOrFail();
return;
}
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("图片Uri为空");
return;
}
LogUtils.d(TAG, "【选图回调】Uri : " + selectedImage.toString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getContentResolver().takePersistableUriPermission(
selectedImage,
Intent.FLAG_GRANT_READ_URI_PERMISSION
);
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
}
mBgSourceUtils.createCropFileProviderBackgroundBean(selectedImage);
LogUtils.d(TAG, "【选图同步】路径绑定完成");
// 选图后启动固定比例裁剪(调用工具类)
mBgSourceUtils.loadSettings();
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
mBgSourceUtils.getPreviewBackgroundBean(),
mBackgroundView.getWidth(),
mBackgroundView.getHeight(),
false,
REQUEST_CROP_IMAGE
);
}
private void handleCropImageResult(int requestCode, int resultCode, Intent data) {
LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果");
File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath());
boolean isFileExist = cropTempFile.exists();
boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false;
long fileSize = isFileExist ? cropTempFile.length() : 0;
boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100;
if (isCropSuccess) {
LogUtils.d(TAG, "handleCropImageResult: 裁剪成功");
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(true);
mBgSourceUtils.saveSettings();
float systemFileRatio = getRatioFromSystemCropFile(cropTempFile);
if (systemFileRatio > 0) {
Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
if (cropBitmap != null && !cropBitmap.isRecycled()) {
Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio);
saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
if (scaledCropBitmap != cropBitmap && !scaledCropBitmap.isRecycled()) {
scaledCropBitmap.recycle();
}
if (!cropBitmap.isRecycled()) {
cropBitmap.recycle();
}
}
}
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing()) {
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
LogUtils.d(TAG, "handleCropImageResult: 裁剪图片加载完成");
}
}
}, 50);
} else {
handleOperationCancelOrFail();
}
}
private float getRatioFromSystemCropFile(File systemCropFile) {
LogUtils.d(TAG, "getRatioFromSystemCropFile: 读取比例");
if (systemCropFile == null || !systemCropFile.exists() || !systemCropFile.isFile()) {
LogUtils.e(TAG, "getRatioFromSystemCropFile: 文件无效");
return -1;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(systemCropFile.getAbsolutePath(), options);
int cropWidth = options.outWidth;
int cropHeight = options.outHeight;
if (cropWidth <= 0 || cropHeight <= 0) {
LogUtils.e(TAG, "getRatioFromSystemCropFile: 尺寸无效");
return -1;
}
float systemRatio = (float) cropWidth / cropHeight;
LogUtils.d(TAG, "getRatioFromSystemCropFile: 比例:" + systemRatio);
return systemRatio;
}
private Bitmap adjustBitmapToFinalRatio(Bitmap originalBitmap, float finalCropRatio) {
LogUtils.d(TAG, "adjustBitmapToFinalRatio: 调整比例");
if (originalBitmap == null || originalBitmap.isRecycled() || finalCropRatio <= 0) {
LogUtils.e(TAG, "adjustBitmapToFinalRatio: 参数无效");
return originalBitmap;
}
int originalWidth = originalBitmap.getWidth();
int originalHeight = originalBitmap.getHeight();
float originalRatio = (float) originalWidth / originalHeight;
if (Math.abs(originalRatio - finalCropRatio) < 0.001f) {
LogUtils.d(TAG, "adjustBitmapToFinalRatio: 比例一致");
return originalBitmap;
}
int targetWidth, targetHeight;
targetHeight = originalHeight;
targetWidth = Math.round(targetHeight * finalCropRatio);
if (targetWidth > originalWidth) {
targetWidth = originalWidth;
targetHeight = Math.round(targetWidth / finalCropRatio);
}
targetWidth = Math.round(targetHeight * finalCropRatio);
LogUtils.d(TAG, "adjustBitmapToFinalRatio: 调整前:" + originalWidth + "x" + originalHeight + ",调整后:" + targetWidth + "x" + targetHeight);
Bitmap adjustedBitmap = Bitmap.createBitmap(
originalBitmap,
(originalWidth - targetWidth) / 2,
(originalHeight - targetHeight) / 2,
targetWidth,
targetHeight
);
return adjustedBitmap;
}
private void saveScaledBitmapToFile(Bitmap bitmap, File targetFile) {
LogUtils.d(TAG, "saveScaledBitmapToFile: 保存图片");
if (bitmap == null || bitmap.isRecycled() || targetFile == null) {
LogUtils.e(TAG, "saveScaledBitmapToFile: 参数无效");
return;
}
if (targetFile.exists()) {
boolean deleteSuccess = targetFile.delete();
LogUtils.d(TAG, "saveScaledBitmapToFile: 删除原文件:" + (deleteSuccess ? "成功" : "失败"));
}
OutputStream outputStream = null;
try {
outputStream = new BufferedOutputStream(new FileOutputStream(targetFile));
bitmap.compress(CompressFormat.JPEG, 100, outputStream);
outputStream.flush();
LogUtils.d(TAG, "saveScaledBitmapToFile: 保存成功");
} catch (IOException e) {
LogUtils.e(TAG, "saveScaledBitmapToFile: 异常:" + e.getMessage());
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "saveScaledBitmapToFile: 关闭流异常");
}
}
}
}
private Bitmap parseCropTempFileToBitmap(File cropTempFile) {
LogUtils.d(TAG, "parseCropTempFileToBitmap: 解析文件");
if (cropTempFile == null || !cropTempFile.exists() || !cropTempFile.isFile() || cropTempFile.length() <= 100) {
LogUtils.e(TAG, "parseCropTempFileToBitmap: 文件无效");
return null;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options);
int maxSize = 2048;
int sampleSize = 1;
while (options.outWidth / sampleSize > maxSize || options.outHeight / sampleSize > maxSize) {
sampleSize *= 2;
}
sampleSize = Math.min(sampleSize, 16);
LogUtils.d(TAG, "parseCropTempFileToBitmap: 采样率:" + sampleSize);
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inInputShareable = true;
Bitmap cropBitmap = null;
try {
cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options);
if (cropBitmap == null || cropBitmap.isRecycled()) {
LogUtils.e(TAG, "parseCropTempFileToBitmap: 解析失败");
return null;
}
LogUtils.d(TAG, "parseCropTempFileToBitmap: 解析成功");
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "parseCropTempFileToBitmap: OOM");
Toast.makeText(this, "图片解析失败", Toast.LENGTH_SHORT).show();
return null;
} catch (Exception e) {
LogUtils.e(TAG, "parseCropTempFileToBitmap: 异常:" + e.getMessage());
return null;
}
return cropBitmap;
}
private void doubleRefreshPreview() {
LogUtils.d(TAG, "【双重刷新】开始");
if (mBackgroundView != null && !isFinishing()) {
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
LogUtils.d(TAG, "【双重刷新】第一重完成");
} else {
LogUtils.w(TAG, "【双重刷新】跳过");
return;
}
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing()) {
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
LogUtils.d(TAG, "【双重刷新】第二重完成");
}
}
}, 50);
}
private void handleOperationCancelOrFail() {
initBackgroundViewByPreviewBean();
LogUtils.d(TAG, "【操作回调】取消或失败");
ToastUtils.show("操作已取消");
}
/**
* 启动系统裁剪工具
* @param activity 上下文
* @param srcFile 裁剪原图
* @param aspectX 裁剪宽比例自由裁剪传0
* @param aspectY 裁剪高比例自由裁剪传0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
/**
* 启动系统裁剪工具
* @param activity 上下文
* @param srcFile 裁剪原图
* @param aspectX 裁剪宽比例自由裁剪传0
* @param aspectY 裁剪高比例自由裁剪传0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
/**
* 获取FileProvider Uri复用方法避免重复代码
*/
public Uri getFileProviderUri(File file) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider";
return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file);
} else {
return Uri.fromFile(file);
}
} catch (Exception e) {
LogUtils.e(TAG, "getFileProviderUri: 生成Uri失败" + e.getMessage());
return null;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
LogUtils.d(TAG, "【权限回调】转发处理");
mPermissionUtils.handleStoragePermissionResult(this, requestCode, permissions, grantResults);
}
@Override
public void finish() {
if (isCommitSettings) {
super.finish();
} else {
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onYes() {
mBgSourceUtils.commitPreviewSourceToCurrent();
isCommitSettings = true;
finish();
}
@Override
public void onNo() {
isCommitSettings = true;
finish();
}
});
}
}
}

View File

@@ -12,7 +12,7 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.beans.BatteryInfoBean;
import cc.winboll.studio.powerbell.model.BatteryInfoBean;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.StringUtils;

View File

@@ -24,10 +24,10 @@ import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.AToolbar;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -193,10 +193,10 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
public void onClick(View v) {
dialog.dismiss();
// 可以在这里添加确定后的回调逻辑
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getCurrentBackgroundBean();
bean.setPixelColor(pixelColor);
utils.saveData();
utils.saveSettings();
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
setBackgroundColor();
}
@@ -217,8 +217,8 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
void setBackgroundColor() {
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getCurrentBackgroundBean();
int nPixelColor = bean.getPixelColor();
RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
mainLayout.setBackgroundColor(nPixelColor);
@@ -235,7 +235,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
Intent intent = new Intent();
intent.setClass(this, BackgroundPictureActivity.class);
intent.setClass(this, BackgroundSettingsActivity.class);
startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), );
return true;
@@ -248,7 +248,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
public void onBackPressed() {
super.onBackPressed();
Intent intent = new Intent();
intent.setClass(this, BackgroundPictureActivity.class);
intent.setClass(this, BackgroundSettingsActivity.class);
startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
}

View File

@@ -0,0 +1,31 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/27 14:26
* @Describe 应用设置窗口
*/
public class SettingsActivity extends Activity {
public static final String TAG = "SettingsActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
}
public void onCheckPermission(View view) {
//ToastUtils.show("onCheckPermission");
if(PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
ToastUtils.show("【权限检查】存储权限已全部获取");
}
}
}

View File

@@ -12,7 +12,7 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
import cc.winboll.studio.powerbell.beans.BatteryData;
import cc.winboll.studio.powerbell.model.BatteryData;
import java.util.ArrayList;
import java.util.List;

View File

@@ -1,99 +0,0 @@
package cc.winboll.studio.powerbell.beans;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 11:52:28
* @Describe 应用背景图片数据类
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
public class BackgroundPictureBean extends BaseBean {
public static final String TAG = "BackgroundPictureBean";
int backgroundWidth = 100;
int backgroundHeight = 100;
boolean isUseBackgroundFile = false;
// 图片拾取像素颜色
int pixelColor = 0;
public BackgroundPictureBean() {
}
public BackgroundPictureBean(String recivedFileName, boolean isUseBackgroundFile) {
this.isUseBackgroundFile = isUseBackgroundFile;
}
public void setPixelColor(int pixelColor) {
this.pixelColor = pixelColor;
}
public int getPixelColor() {
return pixelColor;
}
public void setBackgroundWidth(int backgroundWidth) {
this.backgroundWidth = backgroundWidth;
}
public int getBackgroundWidth() {
return backgroundWidth;
}
public void setBackgroundHeight(int backgroundHeight) {
this.backgroundHeight = backgroundHeight;
}
public int getBackgroundHeight() {
return backgroundHeight;
}
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
this.isUseBackgroundFile = isUseBackgroundFile;
}
public boolean isUseBackgroundFile() {
return isUseBackgroundFile;
}
@Override
public String getName() {
return BackgroundPictureBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
BackgroundPictureBean bean = this;
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
jsonWriter.name("pixelColor").value(bean.getPixelColor());
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
BackgroundPictureBean bean = new BackgroundPictureBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("backgroundWidth")) {
bean.setBackgroundWidth(jsonReader.nextInt());
} else if (name.equals("backgroundHeight")) {
bean.setBackgroundHeight(jsonReader.nextInt());
} else if (name.equals("isUseBackgroundFile")) {
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
} else if (name.equals("pixelColor")) {
bean.setPixelColor(jsonReader.nextInt());
} else {
jsonReader.skipValue();
}
}
// 结束 JSON 对象
jsonReader.endObject();
return bean;
}
}

View File

@@ -12,8 +12,8 @@ import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.UriUtil;
import java.io.File;
@@ -29,7 +29,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
public static final String TAG = "BackgroundPicturePreviewDialog";
Context mContext;
BackgroundPictureUtils mBackgroundPictureUtils;
BackgroundSourceUtils mBackgroundPictureUtils;
Button dialogbackgroundpicturepreviewButton1;
Button dialogbackgroundpicturepreviewButton2;
String mszPreReceivedFileName;
@@ -40,7 +40,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
initEnv();
mContext = context;
mBackgroundPictureUtils = ((BackgroundPictureActivity)context).mBackgroundPictureUtils;
mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
copyAndViewRecivePicture(imageView);
@@ -78,7 +78,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
void copyAndViewRecivePicture(ImageView imageView) {
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext());
BackgroundPictureActivity activity = ((BackgroundPictureActivity)mContext);
BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext);
//取出文件uri
Uri uri = activity.getIntent().getData();
@@ -95,7 +95,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
File fSrcImage = new File(szSrcImage);
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
File mfPreReceivedPhoto = new File(activity.mBackgroundPictureUtils.getBackgroundDir(), mszPreReceivedFileName);
File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
// 复制源图片到剪裁文件
try {
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);

View File

@@ -15,11 +15,12 @@ import androidx.appcompat.app.AlertDialog;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.PictureUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import cc.winboll.studio.powerbell.utils.ImageDownloader;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -41,15 +42,17 @@ public class NetworkBackgroundDialog extends AlertDialog {
private Button btnConfirm;
private Button btnPreview;
private EditText etURL;
BackgroundView bvBackgroundPreview;
BackgroundView mBackgroundView;
Context mContext;
// 主线程 Handler用于接收子线程消息并更新 UI
private Handler mUiHandler;
String previewFilePath;
String mPreviewFilePath;
String mPreviewFileUrl;
String mDownloadSavedPath;
// 按钮点击回调接口Java7 接口实现)
public interface OnDialogClickListener {
void onConfirm(); // 确认按钮点击
void onConfirm(String szConfirmFilePath, String previewFileUrl); // 确认按钮点击
void onCancel(); // 取消按钮点击
}
@@ -87,12 +90,12 @@ public class NetworkBackgroundDialog extends AlertDialog {
switch (msg.what) {
case MSG_IMAGE_LOAD_SUCCESS:
// 图片加载成功,获取文件路径并设置背景
String filePath = (String) msg.obj;
setBackgroundFromPath(filePath);
mDownloadSavedPath = (String) msg.obj;
previewBackground(mDownloadSavedPath);
break;
case MSG_IMAGE_LOAD_FAILED:
// 图片加载失败,设置默认背景
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
ToastUtils.show("图片预览失败,请检查链接");
break;
}
@@ -134,8 +137,9 @@ public class NetworkBackgroundDialog extends AlertDialog {
btnConfirm = (Button) dialogView.findViewById(R.id.btn_confirm);
btnPreview = (Button) dialogView.findViewById(R.id.btn_preview);
etURL = (EditText) dialogView.findViewById(R.id.et_url);
bvBackgroundPreview = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
// 加载初始图片
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
// 设置按钮点击事件
setButtonClickListeners();
}
@@ -149,6 +153,9 @@ public class NetworkBackgroundDialog extends AlertDialog {
@Override
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "取消按钮点击");
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
utils.setCurrentSourceToPreview();
dismiss(); // 关闭对话框
if (listener != null) {
listener.onCancel();
@@ -162,11 +169,12 @@ public class NetworkBackgroundDialog extends AlertDialog {
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击");
// 确定预览背景资源
bvBackgroundPreview.saveToBackgroundSources(previewFilePath);
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
dismiss(); // 关闭对话框
if (listener != null) {
listener.onConfirm();
listener.onConfirm(mPreviewFilePath, mPreviewFileUrl);
}
}
});
@@ -175,14 +183,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
btnPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "确认预览点击");
downloadImageToAlbumAndPreview();
/*String url = etURL.getText().toString().trim();
if (url.isEmpty()) {
ToastUtils.show("请输入图片链接");
return;
}
ImageDownloader.getInstance(mContext).downloadImage(url, mDownloadCallback);*/
}
});
}
@@ -191,26 +192,25 @@ public class NetworkBackgroundDialog extends AlertDialog {
* 根据文件路径设置 BackgroundView 背景(主线程调用)
* @param filePath 图片文件路径
*/
private void setBackgroundFromPath(String filePath) {
private void previewBackground(String previewFilePath) {
FileInputStream fis = null;
try {
File imageFile = new File(filePath);
File imageFile = new File(previewFilePath);
if (!imageFile.exists()) {
LogUtils.e(TAG, "图片文件不存在:" + filePath);
ToastUtils.show("Test");
//bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
ToastUtils.show("图片文件不存在:" + previewFilePath);
LogUtils.e(TAG, "图片文件不存在:" + previewFilePath);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
return;
}
// 预览背景
previewFilePath = filePath;
bvBackgroundPreview.previewBackgroundImage(previewFilePath);
LogUtils.d(TAG, "图片预览成功:" + filePath);
mPreviewFilePath = previewFilePath;
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
mBackgroundView.loadBackgroundBean(utils.getPreviewBackgroundBean());
} catch (Exception e) {
e.printStackTrace();
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
LogUtils.e(TAG, "图片预览失败:" + e.getMessage());
} finally {
// Java7 手动关闭流,避免资源泄漏
@@ -249,40 +249,20 @@ public class NetworkBackgroundDialog extends AlertDialog {
this.listener = listener;
}
/*ImageDownloader.DownloadCallback mDownloadCallback = new ImageDownloader.DownloadCallback() {
@Override
public void onSuccess(String filePath) {
ToastUtils.show("图片下载成功:" + filePath);
LogUtils.d(TAG, filePath);
// 发送消息到主线程,携带图片路径
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, filePath);
mUiHandler.sendMessage(successMsg);
}
@Override
public void onFailure(String errorMsg) {
ToastUtils.show("下载失败:" + errorMsg);
LogUtils.e(TAG, errorMsg);
// 发送图片加载失败消息
mUiHandler.sendEmptyMessage(MSG_IMAGE_LOAD_FAILED);
}
};*/
void downloadImageToAlbumAndPreview() {
//String imgUrl = "https://example.com/test.jpg";
String imgUrl = etURL.getText().toString();
PictureUtils.downloadImageToAlbum(mContext, imgUrl, new PictureUtils.DownloadCallback(){
//String previewFileUrl = "https://example.com/test.jpg";
mPreviewFileUrl = etURL.getText().toString();
ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.DownloadCallback(){
@Override
public void onSuccess(String savePath) {
ToastUtils.show("下载成功:" + savePath);
// 发送消息到主线程,携带图片路径
Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath);
mUiHandler.sendMessage(successMsg);
}
@Override
public void onFailure(Exception e) {
ToastUtils.show("下载失败:" + e.getMessage());
public void onFailure(String errorMsg) {
ToastUtils.show("下载失败:" + errorMsg);
}
});

View File

@@ -19,8 +19,11 @@ import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import cc.winboll.studio.powerbell.views.BatteryDrawable;
@@ -44,6 +47,8 @@ public class MainViewFragment extends Fragment {
Switch mswIsEnableService;
TextView mtvTips;
private BackgroundSourceUtils mBgSourceUtils;
// 背景布局
//LinearLayout mLinearLayoutloadBackground;
@@ -68,7 +73,7 @@ public class MainViewFragment extends Fragment {
TextView mtvUsegeReminderValue;
CheckBox mcbUsegeReminderValue;
TextView mtvCurrentValue;
BackgroundView bvPreviewBackground;
BackgroundView mBackgroundView;
@Override
@@ -76,9 +81,11 @@ public class MainViewFragment extends Fragment {
mView = inflater.inflate(R.layout.fragment_mainview, container, false);
_mMainViewFragment = MainViewFragment.this;
mAppConfigUtils = App.getAppConfigUtils(getActivity());
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
// 获取指定ID的View实例
bvPreviewBackground = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
mBackgroundView = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
loadBackground();
/*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1);
// 注册OnGlobalLayoutListener
@@ -141,6 +148,19 @@ public class MainViewFragment extends Fragment {
return mView;
}
void loadBackground() {
BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean();
mBackgroundView.loadBackgroundBean(bean);
}
@Override
public void onResume() {
super.onResume();
loadBackground();
}
void setViewData() {
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
int nUsegeReminderValue = mAppConfigUtils.getUsegeReminderValue();
@@ -301,22 +321,10 @@ public class MainViewFragment extends Fragment {
}
public void reloadBackground() {
bvPreviewBackground.reloadBackgroundImage();
// BackgroundPictureBean bean = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundPictureBean();
// ImageView imageView = mView.findViewById(R.id.fragmentmainviewImageView1);
// String szBackgroundFilePath = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundDir() + BackgroundPictureActivity.getBackgroundFileName();
// File fBackgroundFilePath = new File(szBackgroundFilePath);
// LogUtils.d(TAG, "szBackgroundFilePath : " + szBackgroundFilePath);
// LogUtils.d(TAG, String.format("fBackgroundFilePath.exists() %s", fBackgroundFilePath.exists()));
// if (bean.isUseBackgroundFile() && fBackgroundFilePath.exists()) {
// Drawable drawableBackground = Drawable.createFromPath(szBackgroundFilePath);
// //drawableBackground.setAlpha(120);
// imageView.setImageDrawable(drawableBackground);
// } else {
// Drawable drawableBackground = getActivity().getDrawable(R.drawable.blank10x10);
// //drawableBackground.setAlpha(120);
// imageView.setImageDrawable(drawableBackground);
// }
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(getActivity());
mBgSourceUtils.loadSettings();
BackgroundBean bean = utils.getCurrentBackgroundBean();
mBackgroundView.loadBackgroundBean(bean);
}
Handler mHandler = new Handler(){

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.beans;
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>

View File

@@ -0,0 +1,254 @@
package cc.winboll.studio.powerbell.model;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 11:52:28
* @Describe 应用背景图片数据类(存储正式/预览背景配置支持JSON序列化/反序列化)
*/
public class BackgroundBean extends BaseBean {
public static final String TAG = "BackgroundPictureBean";
// 核心字段背景图片文件名对应应用私有目录下的图片文件与BackgroundSettingsActivity的_mSourceCroppedFile匹配
private String backgroundFileName = "";
// 核心字段背景图片完整路径解决仅存文件名导致的路径拼接错误与backgroundScaledCompressFilePath对应
private String backgroundFilePath = "";
// 附加字段图片信息如Uri、网络地址等仅作备注不参与路径生成
private String backgroundFileInfo = "";
// 控制字段是否启用背景图片true-显示背景图false-显示透明背景)
private boolean isUseBackgroundFile = false;
// 核心字段压缩后背景图片文件名对应应用私有目录下的压缩图片与saveCropBitmap的压缩图匹配
private String backgroundScaledCompressFileName = "";
// 核心字段压缩后背景图片完整路径解决仅存文件名导致的路径拼接错误适配BackgroundSettingsActivity的私有目录
private String backgroundScaledCompressFilePath = "";
// 重命名字段是否启用压缩背景图原isUseScaledCompress → 新isUseBackgroundScaledCompressFile语义更清晰
private boolean isUseBackgroundScaledCompressFile = false;
// 裁剪比例字段背景图宽高比默认1:1用于固定比例裁剪
private int backgroundWidth = 100;
private int backgroundHeight = 100;
// 像素拾取字段:拾取的像素颜色(用于纯色背景)
private int pixelColor = 0;
/**
* 无参构造器必须JSON反序列化时需默认构造器
*/
public BackgroundBean() {
}
// ====================================== Getter/Setter 方法(全字段,含重命名+新增字段)======================================
public String getBackgroundFileName() {
return backgroundFileName;
}
public void setBackgroundFileName(String backgroundFileName) {
this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName; // 防null避免空指针
}
public String getBackgroundFilePath() {
return backgroundFilePath;
}
public void setBackgroundFilePath(String backgroundFilePath) {
this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath; // 防null避免路径拼接错误
}
public String getBackgroundFileInfo() {
return backgroundFileInfo;
}
public void setBackgroundFileInfo(String backgroundFileInfo) {
this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo; // 防null避免空指针
}
public boolean isUseBackgroundFile() {
return isUseBackgroundFile;
}
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
this.isUseBackgroundFile = isUseBackgroundFile;
}
public String getBackgroundScaledCompressFileName() {
return backgroundScaledCompressFileName;
}
public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) {
this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName; // 防null
}
public String getBackgroundScaledCompressFilePath() {
return backgroundScaledCompressFilePath;
}
public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) {
this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath; // 防null避免路径错误
}
/**
* 重命名原isUseScaledCompress → 新isUseBackgroundScaledCompressFileGetter/Setter同步修改
* 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆
*/
public boolean isUseBackgroundScaledCompressFile() {
return isUseBackgroundScaledCompressFile;
}
public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) {
this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile;
}
public int getBackgroundWidth() {
return backgroundWidth;
}
public void setBackgroundWidth(int backgroundWidth) {
this.backgroundWidth = backgroundWidth <= 0 ? 100 : backgroundWidth; // 防无效值,确保宽高比有效
}
public int getBackgroundHeight() {
return backgroundHeight;
}
public void setBackgroundHeight(int backgroundHeight) {
this.backgroundHeight = backgroundHeight <= 0 ? 100 : backgroundHeight; // 防无效值,确保宽高比有效
}
public int getPixelColor() {
return pixelColor;
}
public void setPixelColor(int pixelColor) {
this.pixelColor = pixelColor;
}
// ====================================== 序列化/反序列化方法(适配重命名字段,兼容旧版本)======================================
@Override
public String getName() {
return BackgroundBean.class.getName(); // 必须重写BaseBean序列化时需类名标识
}
/**
* 序列化同步重命名字段原isUseScaledCompress → 新isUseBackgroundScaledCompressFile
* 确保新字段能正常持久化同时兼容旧版本JSON可选保留旧字段写入避免旧版本读取异常
*/
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
BackgroundBean bean = this;
jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName());
jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath()); // 新增字段:背景原图完整路径
jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo());
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName());
jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath());
// 关键:新字段序列化(核心)
jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile());
// 兼容旧版本保留旧字段名写入可选避免旧版本Bean读取时缺失字段
jsonWriter.name("isUseScaledCompress").value(bean.isUseBackgroundScaledCompressFile());
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
jsonWriter.name("pixelColor").value(bean.getPixelColor());
}
/**
* 反序列化同步处理重命名字段兼容旧版本JSON新旧字段都能读取
* 逻辑:优先读取新字段,若新字段不存在则读取旧字段(确保升级后旧配置仍有效)
*/
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
BackgroundBean bean = new BackgroundBean();
jsonReader.beginObject();
// 临时变量:存储旧字段值(用于兼容)
boolean tempUseScaledCompress = false;
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
switch (name) {
case "backgroundFileName":
bean.setBackgroundFileName(jsonReader.nextString());
break;
case "backgroundFilePath":
bean.setBackgroundFilePath(jsonReader.nextString()); // 新增字段:读取背景原图完整路径
break;
case "backgroundFileInfo":
bean.setBackgroundFileInfo(jsonReader.nextString());
break;
case "isUseBackgroundFile":
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
break;
case "backgroundScaledCompressFileName":
bean.setBackgroundScaledCompressFileName(jsonReader.nextString());
break;
case "backgroundScaledCompressFilePath":
bean.setBackgroundScaledCompressFilePath(jsonReader.nextString());
break;
// 关键:读取新字段(优先)
case "isUseBackgroundScaledCompressFile":
bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean());
break;
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
case "isUseScaledCompress":
tempUseScaledCompress = jsonReader.nextBoolean();
break;
case "backgroundWidth":
bean.setBackgroundWidth(jsonReader.nextInt());
break;
case "backgroundHeight":
bean.setBackgroundHeight(jsonReader.nextInt());
break;
case "pixelColor":
bean.setPixelColor(jsonReader.nextInt());
break;
default:
jsonReader.skipValue(); // 跳过未知字段兼容旧版本Bean避免崩溃
break;
}
}
jsonReader.endObject();
// 兼容逻辑若新字段未被赋值旧版本JSON无此字段则用旧字段值填充
if (!jsonReader.toString().contains("isUseBackgroundScaledCompressFile")) {
bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress);
}
return bean;
}
// ====================================== 辅助方法(同步更新重命名字段)======================================
/**
* 重置背景配置(适配“取消背景”功能,同步重置重命名字段)
*/
public void resetBackgroundConfig() {
this.backgroundFileName = "";
this.backgroundFilePath = ""; // 新增:重置背景原图完整路径
this.backgroundScaledCompressFileName = "";
this.backgroundScaledCompressFilePath = "";
this.backgroundFileInfo = "";
this.isUseBackgroundFile = false;
this.isUseBackgroundScaledCompressFile = false; // 重命名字段重置为false
this.backgroundWidth = 100;
this.backgroundHeight = 100;
}
/**
* 检查背景配置是否有效适配BackgroundSettingsActivity的预览/保存校验)
* 同步使用重命名字段判断压缩图是否启用
* @return true-配置有效可显示背景图false-配置无效
*/
public boolean isBackgroundConfigValid() {
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
if (!isUseBackgroundFile) {
return false;
}
// 原图校验:路径非空 或 文件名非空
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
// 压缩图校验:启用压缩图时,路径/文件名需非空
boolean isCompressValid = true;
if (isUseBackgroundScaledCompressFile()) { // 重命名字段:判断是否启用压缩图
isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty();
}
// 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效
return isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid;
}
}

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.beans;
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.beans;
package cc.winboll.studio.powerbell.model;
import android.util.JsonReader;
import android.util.JsonWriter;

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.beans;
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.beans;
package cc.winboll.studio.powerbell.model;
// 应用消息结构
//

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.beans.AppConfigBean;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;

View File

@@ -23,8 +23,8 @@ import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.beans.AppConfigBean;
import cc.winboll.studio.powerbell.beans.NotificationMessage;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.model.NotificationMessage;
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.services.AssistantService;

View File

@@ -1,48 +0,0 @@
package cc.winboll.studio.powerbell.unittest;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import android.widget.Button;
import cc.winboll.studio.powerbell.MainActivity;
import android.content.Intent;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 18:16
* @Describe BackgroundViewTestFragment
*/
public class BackgroundViewTestFragment extends Fragment {
public static final String TAG = "BackgroundViewTestFragment";
View mainView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//super.onCreateView(inflater, container, savedInstanceState);
// 非调试状态就结束本线程
if (!GlobalApplication.isDebugging()) {
Thread.currentThread().destroy();
}
mainView = inflater.inflate(R.layout.fragment_test_backgroundview, container, false);
((Button)mainView.findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}
});
ToastUtils.show(String.format("%s onCreate", TAG));
return mainView;
}
}

View File

@@ -1,14 +1,23 @@
package cc.winboll.studio.powerbell.unittest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.widget.FrameLayout;
import android.os.Environment;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.powerbell.R;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.nfc.tech.TagTechnology;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -18,22 +27,157 @@ import cc.winboll.studio.libappbase.ToastUtils;
public class MainUnitTestActivity extends AppCompatActivity {
public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0;
// 新增:权限请求码
public static final int REQUEST_STORAGE_PERMISSION = 1001;
View mainView;
BackgroundSourceUtils mBgSourceUtils;
BackgroundView mBackgroundView;
// 测试图片路径用Environment获取适配低版本避免硬编码
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 非调试状态就退出
if (!GlobalApplication.isDebugging()) {
finish();
}
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
setContentView(R.layout.activity_mainunittest);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.activitymainunittestFrameLayout1, new BackgroundViewTestFragment(), BackgroundViewTestFragment.TAG);
fragmentTransaction.commit();
mBackgroundView = findViewById(R.id.backgroundview);
((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
}
});
// 裁剪测试按钮点击事件(新增权限校验)
((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
ToastUtils.show("onClick准备启动裁剪");
LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限");
// 修复1移除高版本API依赖适配低版本存储权限校验
if (checkStoragePermission()) {
// 权限已授予,启动裁剪
startCropTest();
} else {
// 权限未授予,申请权限
requestStoragePermission();
}
}
});
ToastUtils.show(String.format("%s onCreate", TAG));
ToastUtils.show(String.format("%s onCreate", TAG));
// 加载测试图片(验证图片路径是否有效)
loadBackground();
}
/**
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
*/
private void startCropTest() {
// 修复2输出路径用Environment获取确保目录存在避免路径无效
File outputDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/");
if (!outputDir.exists()) {
outputDir.mkdirs(); // 创建目录(避免输出路径不存在导致裁剪失败)
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
}
String dstOutputPath = outputDir.getAbsolutePath()
+ "/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
// 修复3自由裁剪时比例传0避免100:100过大导致机型崩溃
ImageCropUtils.startImageCrop(
MainUnitTestActivity.this,
new File(szTestSource),
new File(dstOutputPath),
0, // 自由裁剪传0
0, // 自由裁剪传0
true,
REQUEST_CROP_IMAGE
);
}
/**
* 校验存储读写权限适配Android 6.0+ 低版本SDK移除TIRAMISU依赖
*/
private boolean checkStoragePermission() {
// 适配Android 6.0API 23及以上用通用的读写权限移除高版本API
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
/**
* 申请存储读写权限适配低版本SDK移除READ_MEDIA_IMAGES依赖
*/
private void requestStoragePermission() {
LogUtils.d(TAG, "【裁剪测试】申请存储读写权限");
// 用通用的读写权限适配所有Android 6.0+ 机型,无高版本依赖)
ActivityCompat.requestPermissions(
this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION
);
}
/**
* 权限申请回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_STORAGE_PERMISSION) {
// 校验权限是否授予
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
ToastUtils.show("存储权限已授予,启动裁剪");
startCropTest(); // 权限授予后启动裁剪
} else {
ToastUtils.show("存储权限被拒绝,无法启动裁剪");
LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝");
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "【裁剪回调】requestCode" + requestCode + "resultCode" + resultCode + "data" + (data == null ? "null" : data.toString()));
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null));
// 裁剪完成后回收权限
if (requestCode == REQUEST_CROP_IMAGE) {
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
//ImageCropUtils.releaseCropPermission(this, outputUri);
mBackgroundView.loadImage(dstOutputPath);
}
}
void loadBackground() {
// 校验测试图片是否存在(避免路径错误)
File testFile = new File(szTestSource);
if (testFile.exists() && testFile.length() > 100) {
mBackgroundView.loadImage(szTestSource);
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource);
} else {
ToastUtils.show("测试图片不存在或无效");
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource);
}
}
}

View File

@@ -2,7 +2,7 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.beans.BatteryInfoBean;
import cc.winboll.studio.powerbell.model.BatteryInfoBean;
import java.util.ArrayList;
public class AppCacheUtils {

View File

@@ -5,8 +5,8 @@ import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.beans.AppConfigBean;
import cc.winboll.studio.powerbell.beans.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.model.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.services.ControlCenterService;

View File

@@ -1,64 +0,0 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 12:07:20
* @Describe 背景图片工具集
*/
import android.content.Context;
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
import java.io.File;
public class BackgroundPictureUtils {
public static final String TAG = "BackgroundPictureUtils";
static BackgroundPictureUtils _mBackgroundPictureUtils;
Context mContext;
BackgroundPictureBean mBackgroundPictureBean;
// 背景图片目录
String mszBackgroundDir;
BackgroundPictureUtils(Context context) {
mContext = context;
String szExternalFilesDir = mContext.getExternalFilesDir(TAG) + File.separator;
setBackgroundDir(szExternalFilesDir + "Background" + File.separator);
loadBackgroundPictureBean();
}
public static BackgroundPictureUtils getInstance(Context context) {
if (_mBackgroundPictureUtils == null) {
_mBackgroundPictureUtils = new BackgroundPictureUtils(context);
}
return _mBackgroundPictureUtils;
}
//
// 加载应用背景图片配置数据
//
public BackgroundPictureBean loadBackgroundPictureBean() {
mBackgroundPictureBean = BackgroundPictureBean.loadBean(mContext, BackgroundPictureBean.class);
if (mBackgroundPictureBean == null) {
mBackgroundPictureBean = new BackgroundPictureBean();
BackgroundPictureBean.saveBean(mContext, mBackgroundPictureBean);
}
return mBackgroundPictureBean;
}
void setBackgroundDir(String mszBackgroundDir) {
this.mszBackgroundDir = mszBackgroundDir;
}
public String getBackgroundDir() {
return mszBackgroundDir;
}
public BackgroundPictureBean getBackgroundPictureBean() {
return mBackgroundPictureBean;
}
public void saveData() {
BackgroundPictureBean.saveBean(mContext, mBackgroundPictureBean);
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More