Compare commits
65 Commits
appbase-v1
...
powerbell
| Author | SHA1 | Date | |
|---|---|---|---|
| eab26dbba2 | |||
| 5d4a5f25ad | |||
| 858b874ea1 | |||
| 2e41aae853 | |||
| 56a9a2f476 | |||
| 2a2c006264 | |||
| df51b415fb | |||
| 19e6e276bd | |||
| 7f3c91fb1d | |||
| 4434221827 | |||
| 75415956eb | |||
| c930308425 | |||
| 8a16728609 | |||
| 59992542c4 | |||
| fe5dd9e1ab | |||
| 0d99057880 | |||
| ca66120e55 | |||
| ca1850fe8a | |||
| 4a31b9eef0 | |||
| c6a6826102 | |||
| 16c44e5e0e | |||
| 8fb7147333 | |||
| e190d3ff39 | |||
| a997fb01c8 | |||
| 637d4577df | |||
| 33f1b430a4 | |||
| f6a00fac36 | |||
| 5d3d46f2fe | |||
| ed660aa4ef | |||
| ee4b0ca6d9 | |||
| 6538ebafef | |||
| 1cadc4ed93 | |||
| 04b8906a96 | |||
| e14744b2ac | |||
| 4e7b7daa42 | |||
| f7ef8f6b19 | |||
| 6951f642a1 | |||
| a6b25eaf2b | |||
| 80363c6b4c | |||
| 66e3e602e5 | |||
| 9b010df881 | |||
| 3c3bcc4ee4 | |||
| 21c712f7b3 | |||
| e408b5cbde | |||
| 5a9469317d | |||
| 0d5f7f40cd | |||
| 64e5bc753a | |||
| 13b6af6921 | |||
| e2703495ae | |||
| c9bc5c88d8 | |||
| 34356b8116 | |||
| de189c3fb0 | |||
| c4b2ecaecb | |||
| 1b53594086 | |||
| 6a2d011ceb | |||
| 5aa54091e5 | |||
| bc8a63867c | |||
| 2a10cb493d | |||
| ea7e2f8366 | |||
| d073a86b9b | |||
| e76427eac8 | |||
| af53216af3 | |||
| 3ae56bb202 | |||
| 09f1974c8e | |||
| 721c93c4b2 |
@@ -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 应用时,
|
||||
|
||||
@@ -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/)
|
||||
|
||||
#### 参考文档
|
||||
#### 参考文档
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
132
aes/proguard-rules.pro
vendored
@@ -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 *;
|
||||
#}
|
||||
|
||||
@@ -35,8 +35,6 @@
|
||||
|
||||
<activity android:name=".TestActivityManagerActivity"/>
|
||||
|
||||
<activity android:name=".SettingsActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -32,7 +32,4 @@
|
||||
<item
|
||||
android:id="@+id/item_drawerfragmentactivity"
|
||||
android:title="Test DrawerFragmentActivity"/>
|
||||
<item
|
||||
android:id="@+id/item_settings"
|
||||
android:title="Settings"/>
|
||||
</menu>
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Nov 30 17:12:48 HKT 2025
|
||||
stageCount=7
|
||||
#Fri Nov 21 11:41:04 HKT 2025
|
||||
stageCount=2
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.6
|
||||
publishVersion=15.11.1
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.7
|
||||
baseBetaVersion=15.11.2
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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/)
|
||||
|
||||
#### 参考文档
|
||||
@@ -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}")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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 {
|
||||
// 定义全局变量,常用于版本管理
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; // 无匹配联系人
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Nov 30 17:12:48 HKT 2025
|
||||
stageCount=7
|
||||
#Fri Nov 21 11:41:04 HKT 2025
|
||||
stageCount=2
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.6
|
||||
publishVersion=15.11.1
|
||||
buildCount=0
|
||||
baseBetaVersion=15.11.7
|
||||
baseBetaVersion=15.11.2
|
||||
|
||||
@@ -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,22 +28,6 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- 崩溃通知复制活动(类库Manifest配置,确保宿主能合并注册) -->
|
||||
<activity
|
||||
android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:launchMode="singleTask"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:exported="true"
|
||||
android:allowTaskReparenting="false"
|
||||
android:clearTaskOnLaunch="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>
|
||||
|
||||
@@ -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;
|
||||
@@ -53,7 +52,7 @@ public final class CrashHandler {
|
||||
public static final String TITTLE = "CrashReport";
|
||||
|
||||
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
|
||||
public static final String EXTRA_CRASH_LOG = "crashInfo";
|
||||
public static final String EXTRA_CRASH_INFO = "crashInfo";
|
||||
|
||||
/** SharedPreferences 存储键(用于记录崩溃状态) */
|
||||
final static String PREFS = CrashHandler.class.getName() + "PREFS";
|
||||
@@ -170,12 +169,12 @@ public final class CrashHandler {
|
||||
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
|
||||
// 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity)
|
||||
intent.setClass(app, GlobalCrashActivity.class);
|
||||
intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
|
||||
intent.putExtra(EXTRA_CRASH_INFO, errorLog); // 传递崩溃日志
|
||||
} else {
|
||||
LogUtils.d(TAG, "gotoCrashActiviy: else");
|
||||
// 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃)
|
||||
intent.setClass(app, CrashActivity.class);
|
||||
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
|
||||
intent.putExtra(EXTRA_CRASH_INFO, errorLog);
|
||||
}
|
||||
|
||||
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
|
||||
@@ -186,17 +185,10 @@ public final class CrashHandler {
|
||||
);
|
||||
|
||||
try {
|
||||
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||||
// 如果是 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();
|
||||
@@ -436,7 +428,7 @@ public final class CrashHandler {
|
||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
|
||||
// 获取传递的崩溃日志
|
||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_INFO);
|
||||
// 设置系统默认主题(避免自定义主题冲突)
|
||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||||
|
||||
|
||||
@@ -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 终止,单例实例已释放");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe
|
||||
.postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
|
||||
// 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
|
||||
|
||||
// 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
|
||||
setContentView(R.layout.activity_globalcrash);
|
||||
|
||||
@@ -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=0,y=0)
|
||||
smoothScrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 true:TAG 名称相同;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); // 降序
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,264 +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 android.text.TextUtils;
|
||||
|
||||
import cc.winboll.studio.libappbase.CrashHandler;
|
||||
import cc.winboll.studio.libappbase.GlobalCrashActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/29 21:12
|
||||
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
|
||||
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
|
||||
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
|
||||
*/
|
||||
public class CrashHandleNotifyUtils {
|
||||
|
||||
public static final String TAG = "CrashHandleNotifyUtils";
|
||||
|
||||
/** 通知渠道ID(Android 8.0+ 必须) */
|
||||
private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel";
|
||||
/** 通知渠道名称(用户可见) */
|
||||
private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知";
|
||||
/** 通知ID(唯一) */
|
||||
public static final int CRASH_NOTIFY_ID = 0x001;
|
||||
/** Android 12 对应 API 版本号(31) */
|
||||
private static final int API_LEVEL_ANDROID_12 = 31;
|
||||
/** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+) */
|
||||
private static final int FLAG_IMMUTABLE = 0x00000040;
|
||||
|
||||
/** 通知内容最大行数(控制在3行,超出部分省略) */
|
||||
private static final int NOTIFICATION_MAX_LINES = 3;
|
||||
|
||||
|
||||
/**
|
||||
* 处理未捕获异常(核心方法,类库入口)
|
||||
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
|
||||
* @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
|
||||
* @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity)
|
||||
* @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来)
|
||||
*/
|
||||
public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) {
|
||||
// 1. 校验核心参数(类库场景必须严格校验,避免空指针)
|
||||
if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) {
|
||||
LogUtils.e(TAG, "发送崩溃通知失败:参数为空(hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆)
|
||||
String hostAppName = getHostAppName(hostApp, hostPackageName);
|
||||
|
||||
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity)
|
||||
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
|
||||
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
|
||||
* @param hostApp 宿主应用的 Application 实例
|
||||
* @param intent 存储崩溃信息的意图(extra 中携带崩溃日志)
|
||||
*/
|
||||
public static void handleUncaughtException(Application hostApp, Intent intent) {
|
||||
// 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名)
|
||||
String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME");
|
||||
if (TextUtils.isEmpty(hostPackageName)) {
|
||||
hostPackageName = hostApp.getPackageName();
|
||||
LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名:" + hostPackageName);
|
||||
}
|
||||
|
||||
// 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
|
||||
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
|
||||
// 调用核心方法处理
|
||||
handleUncaughtException(hostApp, hostPackageName, errorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
|
||||
* @param hostContext 宿主应用的上下文(Application 实例)
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @return 宿主应用名称(读取失败返回 "未知应用")
|
||||
*/
|
||||
private static String getHostAppName(Context hostContext, String hostPackageName) {
|
||||
try {
|
||||
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
|
||||
return hostContext.getPackageManager().getApplicationLabel(
|
||||
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
|
||||
).toString();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + ")", e);
|
||||
return "未知应用";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送崩溃通知到宿主系统通知栏(类库兼容版)
|
||||
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
|
||||
* @param hostContext 宿主应用的上下文(Application 实例)
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @param hostAppName 宿主应用的名称(用于通知标题)
|
||||
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
|
||||
*/
|
||||
private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) {
|
||||
// 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用)
|
||||
NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager == null) {
|
||||
LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 适配 Android 8.0+(API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createCrashNotifyChannel(hostContext, notificationManager);
|
||||
}
|
||||
|
||||
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity)
|
||||
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
|
||||
if (jumpIntent == null) {
|
||||
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
|
||||
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
|
||||
|
||||
// 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆)
|
||||
notificationManager.notify(CRASH_NOTIFY_ID, notification);
|
||||
LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突)
|
||||
* @param hostContext 宿主应用的上下文
|
||||
* @param notificationManager 宿主的通知管理器
|
||||
*/
|
||||
private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) {
|
||||
// 仅 Android 8.0+ 执行(避免低版本报错)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// 构建通知渠道(归属宿主应用,描述明确类库用途)
|
||||
android.app.NotificationChannel channel = new android.app.NotificationChannel(
|
||||
CRASH_NOTIFY_CHANNEL_ID,
|
||||
CRASH_NOTIFY_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)");
|
||||
// 注册渠道到宿主的通知管理器,确保渠道归属宿主
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + ",渠道ID:" + CRASH_NOTIFY_CHANNEL_ID + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键)
|
||||
* 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity;
|
||||
* 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配;
|
||||
* 3. 使用宿主上下文,避免类库上下文导致的适配问题。
|
||||
* @param hostContext 宿主应用的上下文
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
|
||||
* @return 跳转崩溃详情页的 PendingIntent
|
||||
*/
|
||||
private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) {
|
||||
try {
|
||||
// 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名)
|
||||
Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class);
|
||||
// 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity(避免类库包名干扰)
|
||||
crashIntent.setPackage(hostPackageName);
|
||||
// 传递崩溃日志(键:EXTRA_CRASH_INFO,与宿主 GlobalCrashActivity 完全匹配)
|
||||
crashIntent.putExtra(CrashHandler.EXTRA_CRASH_LOG, errorLog);
|
||||
// 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱
|
||||
crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
// 2. 构建 PendingIntent(使用宿主上下文,适配高版本)
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
||||
flags |= FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
hostContext,
|
||||
CRASH_NOTIFY_ID, // 用通知ID作为请求码,确保唯一(避免意图复用)
|
||||
crashIntent,
|
||||
flags
|
||||
);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通知实例(类库兼容版)
|
||||
* 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用
|
||||
* @param hostContext 宿主应用的上下文
|
||||
* @param hostAppName 宿主应用的名称(通知标题)
|
||||
* @param errorLog 崩溃日志(通知内容)
|
||||
* @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity)
|
||||
* @return 构建完成的 Notification 对象
|
||||
*/
|
||||
private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) {
|
||||
// 兼容 Android 8.0+:指定宿主的通知渠道ID
|
||||
Notification.Builder builder = new Notification.Builder(hostContext);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
|
||||
}
|
||||
|
||||
// 核心:用BigTextStyle控制“默认3行省略,下拉显示完整”(使用宿主上下文构建)
|
||||
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
|
||||
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
|
||||
bigTextStyle.bigText(errorLog);
|
||||
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
|
||||
builder.setStyle(bigTextStyle);
|
||||
|
||||
// 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主)
|
||||
builder
|
||||
// 关键:使用宿主应用的小图标(避免类库图标显示异常)
|
||||
.setSmallIcon(hostContext.getApplicationInfo().icon)
|
||||
.setContentTitle(hostAppName + " 崩溃")
|
||||
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
|
||||
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
|
||||
.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 {
|
||||
return builder.getNotification();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:截取日志文本,确保显示在3行内(通用逻辑,无包名依赖)
|
||||
* @param content 完整崩溃日志
|
||||
* @return 3行内的缩略文本
|
||||
*/
|
||||
private static String getShortContent(String content) {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return "无崩溃日志";
|
||||
}
|
||||
int maxLength = 80; // 估算3行字符数(可根据需求调整)
|
||||
return content.length() <= maxLength ? content : content.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(类库场景:空实现,避免宿主调用时报错,预留扩展)
|
||||
* @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖)
|
||||
*/
|
||||
public static void release(Context hostContext) {
|
||||
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + ")");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -4,5 +4,4 @@
|
||||
<color name="colorPrimaryDark">#FF005C12</color>
|
||||
<color name="colorAccent">#FF8DFFA2</color>
|
||||
<color name="colorText">#FFFFFB8D</color>
|
||||
<!-- 通知按钮颜色(启用/禁用) -->
|
||||
</resources>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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("【权限检查】存储权限已全部获取");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -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 → 新isUseBackgroundScaledCompressFile(Getter/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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -1,4 +1,4 @@
|
||||
package cc.winboll.studio.powerbell.beans;
|
||||
package cc.winboll.studio.powerbell.model;
|
||||
|
||||
// 应用消息结构
|
||||
//
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.0(API 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user