Compare commits

..

24 Commits

Author SHA1 Message Date
eb0cba7005 <positions>APK 15.12.6 release Publish. 2025-12-09 10:37:45 +08:00
51cdbfefab <positions>APK 15.12.5 release Publish. 2025-12-09 10:36:26 +08:00
e07e5fa8a8 <positions>Start New Stage Version. 2025-12-09 10:34:23 +08:00
5901cc5d75 调整启动页按钮风格 2025-12-09 10:33:15 +08:00
a7617a378c <positions>APK 15.12.4 release Publish. 2025-12-08 16:14:14 +08:00
b0dfb4be76 <positions>Start New Stage Version. 2025-12-08 16:13:21 +08:00
07859f316f 编译参数修复 2025-12-08 16:12:49 +08:00
08fc5a47cd 主窗口添加背景边框。 2025-12-08 16:11:09 +08:00
c423ac146f <positions>APK 15.12.3 release Publish. 2025-12-08 14:36:50 +08:00
122722ffd0 <positions>Start New Stage Version. 2025-12-08 14:35:17 +08:00
ee9fb51879 精简主界面UI 2025-12-08 14:28:17 +08:00
c3688866a0 代码优化 2025-12-08 14:26:40 +08:00
d0b51ac7c8 <positions>APK 15.12.2 release Publish. 2025-12-08 00:07:16 +08:00
0645361bbf 添加编译包混淆配置 2025-12-08 00:06:04 +08:00
ae92689e04 <positions>APK 15.12.1 release Publish. 2025-12-07 23:58:27 +08:00
0dac650877 添加米盟广告SDK,添加主题风格设置。 2025-12-07 23:56:52 +08:00
7b54a6ec0f <positions>APK 15.12.0 release Publish. 2025-12-07 21:00:59 +08:00
bdb94b5c18 核心源码回溯到15.0.16版本,附加添加一些最新的概念设计文件。 2025-12-07 20:45:24 +08:00
7563ea01f9 添加提交点c725576d58c2cbe486754c3909a6a4d8bfd3797c时的差异代码。 2025-12-07 20:35:02 +08:00
13b171915e 添加提交点c725576d58c2cbe486754c3909a6a4d8bfd3797c时的附加提交文件。 2025-12-07 20:34:09 +08:00
d38407099d 编译模块修复1 2025-12-07 20:20:35 +08:00
af8d2b9d52 恢复上一个可编译版本。 2025-12-07 20:04:52 +08:00
d7a6d8d4a3 positions 分支恢复到15.0.16版本,提交点6376ff4ccfa5955ae7d6a109a8a758c904a73b67 2025-12-07 19:53:23 +08:00
c725576d58 设置编译版本与基础类库 2025-12-07 19:42:09 +08:00
105 changed files with 4603 additions and 8264 deletions

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<application>
<!-- Put flavor specific code here -->
</application>
</manifest>

View File

@@ -1,25 +0,0 @@
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.5.0
Name: AndroidManifest.xml
SHA1-Digest: U36A0NWthb49+Rxs33tkIuNYFCI=
Name: jni/Android.mk
SHA1-Digest: ZpGSlRJPL0g9OejiWbQorqj40/Y=
Name: jni/Application.mk
SHA1-Digest: TKh2CbRLeKfvgL4cPfmoxcVz+vc=
Name: jni/hello-jni.cpp
SHA1-Digest: 1btXO19SqB6rDvPo5ynG0brDqTk=
Name: project.properties
SHA1-Digest: 0ekOiGTFMVJOWqAFzNFj/1vxPL8=
Name: res/values/strings.xml
SHA1-Digest: FgRO/zbNaC1wuZKVT7h6NSYBmpY=
Name: src/$package_name$/HelloJni.java
SHA1-Digest: p0e9DNKocjRnsOhetb9bnp9s9J4=

View File

@@ -32,21 +32,28 @@ android {
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.11"
versionName "15.12"
if(true) {
versionName = genVersionName("${versionName}")
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
// 米盟 SDK
packagingOptions {
doNotStrip "*/*/libmimo_1011.so"
}
}
dependencies {
// 米盟
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
//注意以下5个库必须要引入
//api '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'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// https://mvnrepository.com/artifact/com.jzxiang.pickerview/TimePickerDialog
api 'com.jzxiang.pickerview:TimePickerDialog:1.0.1'
@@ -72,7 +79,14 @@ dependencies {
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'
api 'cc.winboll.studio:libaes:15.11.0'
api 'cc.winboll.studio:libappbase:15.11.0'
// WinBoLL库 nexus.winboll.cc 地址
//api 'cc.winboll.studio:libaes:15.12.0'
//api 'cc.winboll.studio:libappbase:15.12.2'
// WinBoLL备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
api fileTree(dir: 'libs', include: ['*.jar'])
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Nov 15 08:45:48 GMT 2025
stageCount=2
#Tue Dec 09 10:37:45 HKT 2025
stageCount=7
libraryProject=
baseVersion=15.11
publishVersion=15.11.1
buildCount=21
baseBetaVersion=15.11.2
baseVersion=15.12
publishVersion=15.12.6
buildCount=0
baseBetaVersion=15.12.7

View File

@@ -1,21 +1,143 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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 *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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.**
# ============================== 必要补充规则 ==============================
# OkHttp 4.4.1 补充规则Java 7 兼容)
-keep class okhttp3.internal.concurrent.** { *; }
-keep class okhttp3.internal.connection.** { *; }
-dontwarn okhttp3.internal.concurrent.TaskRunner
-dontwarn okhttp3.internal.connection.RealCall
# 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

View File

@@ -3,9 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.positions">
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 只有在前台运行时才能获取大致位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
@@ -30,6 +27,9 @@
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-feature
android:name="android.hardware.location.gps"
android:required="false"/>
@@ -65,8 +65,6 @@
</activity>
<activity android:name=".activities.CrashActivity"/>
<activity-alias
android:name=".MainActivityWukong"
android:targetActivity=".MainActivity"
@@ -111,15 +109,9 @@
</activity-alias>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.positions.activities.LocationActivity"/>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<activity android:name="cc.winboll.studio.positions.activities.ShortcutActionActivity"/>
<service
android:name=".services.MainService"
@@ -143,6 +135,14 @@
</receiver>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -151,11 +151,11 @@
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
android:resource="@xml/file_provider"/>
</provider>
<activity android:name="cc.winboll.studio.positions.activities.ShortcutActionActivity"/>
<activity android:name="cc.winboll.studio.positions.activities.SettingsActivity"/>
</application>

View File

@@ -14,6 +14,7 @@ import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
@@ -24,7 +25,6 @@ import android.widget.Toast;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.utils.MyActivityLifecycleCallbacks;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
@@ -44,36 +44,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class App extends GlobalApplication {
public static volatile AppLevel _mAppLevel = AppLevel.WUKONG;
public static final String COMPONENT_WUKONG = "cc.winboll.studio.positions.MainActivityWukong";
public static final String COMPONENT_LAOJUN = "cc.winboll.studio.positions.MainActivityLaojun";
public static final String ACTION_OPEN_APPPLUS = "cc.winboll.studio.positions.App.ACTION_OPEN_APPPLUS";
public static final String ACTION_CLOSE_APPPLUS = "cc.winboll.studio.positions.App.ACTION_CLOSE_APPPLUS";
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
MyActivityLifecycleCallbacks mMyActivityLifecycleCallbacks;
@Override
public void onCreate() {
super.onCreate();
setIsDebugging(BuildConfig.DEBUG);
WinBoLLActivityManager.init(this);
// 初始化 Toast 框架
ToastUtils.init(this);
// 设置 Toast 布局样式
//ToastUtils.setView(R.layout.view_toast);
//ToastUtils.setStyle(new WhiteToastStyle());
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
//CrashHandler.getInstance().registerGlobal(this);
//CrashHandler.getInstance().registerPart(this);
mMyActivityLifecycleCallbacks = new MyActivityLifecycleCallbacks();
registerActivityLifecycleCallbacks(mMyActivityLifecycleCallbacks);
}
public static void write(InputStream input, OutputStream output) throws IOException {

View File

@@ -1,43 +0,0 @@
package cc.winboll.studio.positions;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/10 07:23
* @Describe 应用级别类型枚举
*/
public enum AppLevel {
WUKONG("wukong", "悟空级别"),
LAOJUN("laojun", "老君级别");
public static final String TAG = "AppLevel";
// 枚举属性
private final String code; // 编码(如 "wukong"
private final String desc; // 描述
// 构造方法Java 7 需显式定义)
AppLevel(String code, String desc) {
this.code = code;
this.desc = desc;
}
// Getter 方法(获取枚举属性)
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
// 可选:根据 code 获取枚举项(便于业务使用)
public static AppLevel getByCode(String code) {
for (AppLevel level : values()) {
if (level.code.equals(code)) {
return level;
}
}
return null; // 或抛出异常,根据业务需求调整
}
}

View File

@@ -4,9 +4,12 @@ import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.CompoundButton;
@@ -16,15 +19,17 @@ import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.R;
import cc.winboll.studio.positions.activities.LocationActivity;
import cc.winboll.studio.positions.activities.SettingsActivity;
import cc.winboll.studio.positions.activities.WinBoLLActivity;
import cc.winboll.studio.positions.utils.APPPlusUtils;
import cc.winboll.studio.positions.utils.AppConfigsUtil;
import cc.winboll.studio.positions.utils.JsonShareHandler;
import cc.winboll.studio.positions.utils.ServiceUtil;
import android.widget.LinearLayout;
/**
* 主页面:仅负责
@@ -34,8 +39,7 @@ import cc.winboll.studio.positions.utils.ServiceUtil;
*/
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "MainActivity";
// 权限请求码(建议定义为类常量,避免魔法值)
// 权限请求码(建议定义为类常量,避免魔法值)
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
@@ -45,7 +49,8 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
private Toolbar mToolbar;
// 服务相关:服务实例、绑定状态标记
//private DistanceRefreshService mDistanceService;
//private boolean isServiceBound = false;
private boolean isServiceBound = false;
ADsBannerView mADsBannerView;
@Override
@@ -87,10 +92,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 关联主页面布局
// 处理启动时的分享 Intent
handleShareIntent(getIntent());
// 1. 初始化顶部 Toolbar保留原逻辑设置页面标题
initToolbar();
// 2. 初始化其他控件
@@ -101,36 +102,32 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
}
// 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
//bindDistanceService();
mADsBannerView = findViewById(R.id.adsbanner);
setLLMainBackgroundColor();
}
// 在 Activity 的 onCreate() 或需要获取颜色的方法中调用
private void setLLMainBackgroundColor() {
// 1. 定义要解析的主题属性(这里是 colorAccent
TypedArray a = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
// 2. 获取对应的颜色值(默认值可设为你需要的 fallback 颜色,如 Color.GRAY
int colorAccent = a.getColor(0, Color.GRAY);
// 3. 必须回收,避免内存泄漏
a.recycle();
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// 处理后续接收的分享 Intent如应用已在后台
handleShareIntent(intent);
}
private void handleShareIntent(Intent intent) {
if (intent != null && Intent.ACTION_SEND.equals(intent.getAction())) {
// 调用工具类,弹出确认对话框
JsonShareHandler.handleSharedJsonWithConfirm(this, intent, new JsonShareHandler.ConfirmCallback() {
@Override
public void onConfirm(boolean isConfirm) {
// 回调处理isConfirm 为 true 表示接收并保存false 表示取消
if (!isConfirm) {
Log.d("MainActivity", "用户取消接收文件");
// 可添加取消后的逻辑(如关闭页面)
// finish();
}
}
});
}
LinearLayout llmain = findViewById(R.id.llmain);
llmain.setBackgroundColor(colorAccent);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mADsBannerView != null) {
mADsBannerView.releaseAdResources();
}
// 页面销毁时解绑服务避免Activity与服务相互引用导致内存泄漏
// if (isServiceBound) {
// unbindService(mServiceConn);
@@ -139,6 +136,16 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
// }
}
@Override
protected void onResume() {
super.onResume();
if (mADsBannerView != null) {
mADsBannerView.resumeADs(MainActivity.this);
}
}
// ---------------------- 核心功能1初始化UI组件Toolbar + 服务开关) ----------------------
/**
* 初始化顶部 Toolbar设置页面标题
@@ -147,9 +154,9 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
setSupportActionBar(mToolbar);
// 给ActionBar设置标题先判断非空避免空指针异常
AppLevel appLevel = AppConfigsUtil.getInstance(getApplicationContext()).getAppLevel(true);
getSupportActionBar().setTitle(getString(R.string.app_name));
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(getString(R.string.app_name));
}
}
/**
@@ -161,7 +168,7 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
mManagePositionsButton.setEnabled(mServiceSwitch.isChecked());
// Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
@@ -188,6 +195,39 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 主题菜单
AESThemeUtil.inflateMenu(this, menu);
// 调试工具菜单
if (App.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
}
// 应用其他菜单
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int menuItemId = item.getItemId();
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
recreate();
} if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
} else if (menuItemId == R.id.item_settings) {
Intent intent = new Intent();
intent.setClass(this, SettingsActivity.class);
startActivity(intent);
} else {
// 在switch语句中处理每个ID并在处理完后返回true未处理的情况返回false。
return super.onOptionsItemSelected(item);
}
return true;
}
/**
* 绑定服务(仅用于获取服务状态,不启动服务)
*/
@@ -208,14 +248,6 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
startActivity(new Intent(MainActivity.this, LocationActivity.class));
}
/**
* 跳转至“日志页LogActivity按钮点击触发需在布局中设置 android:onClick="onLog"
* 无服务状态限制,直接跳转
*/
public void onLog(View view) {
WinBoLLActivityManager.getInstance().startLogActivity(this); // 调用LogActivity静态方法跳转保留原逻辑
}
// ---------------------- 新增位置权限处理适配Java7 + 后台GPS权限 ----------------------
/**
* 检查是否拥有「前台+后台」位置权限适配Android版本差异

View File

@@ -1,22 +0,0 @@
package cc.winboll.studio.positions;
import android.os.Bundle;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/13 15:21
* @Describe MainActivityLaojun
*/
public class MainActivityLaojun extends MainActivity {
public static final String TAG = "MainActivityLaojun";
@Override
protected void onCreate(Bundle savedInstanceState) {
ToastUtils.show("道法自然");
LogUtils.d(TAG, "玩法归臻");
super.onCreate(savedInstanceState);
}
}

View File

@@ -1,43 +0,0 @@
package cc.winboll.studio.positions;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/15 15:14
* @Describe 应用入口级别类型枚举
*/
public enum PointLevel {
DORAEMON("doraemon", "叮铛级别"),
WUKONG("wukong", "悟空级别"),
LAOJUN("laojun", "老君级别");
public static final String TAG = "PointLevel";
// 枚举属性
private final String code; // 编码(如 "wukong"
private final String desc; // 描述
// 构造方法Java 7 需显式定义)
PointLevel(String code, String desc) {
this.code = code;
this.desc = desc;
}
// Getter 方法(获取枚举属性)
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
// 可选:根据 code 获取枚举项(便于业务使用)
public static PointLevel getByCode(String code) {
for (PointLevel level : values()) {
if (level.code.equals(code)) {
return level;
}
}
return null; // 或抛出异常,根据业务需求调整
}
}

View File

@@ -16,8 +16,10 @@ import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.R;
@@ -33,12 +35,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
* 2. Adapter 初始化传入 MainService 实例,确保数据来源唯一
* 3. 所有位置/任务操作通过 MainService 接口执行
*/
public class LocationActivity extends Activity {
public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "LocationActivity";
private Toolbar mToolbar;
private RecyclerView mRvPosition;
private PositionAdapter mPositionAdapter;
// MainService 引用+绑定状态AtomicBoolean 确保多线程状态可见性)
private MainService mMainService;
private final AtomicBoolean isServiceBound = new AtomicBoolean(false);
@@ -96,11 +100,34 @@ public class LocationActivity extends Activity {
}
};
@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_location);
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
// 1. 初始化视图优先执行避免Adapter初始化时视图为空
initView();
// 2. 初始化GPS监听提前创建避免绑定服务后空指针
@@ -167,7 +194,7 @@ public class LocationActivity extends Activity {
}
}
LogUtils.d(TAG, "数据同步完成:服务位置数=" + (servicePosList == null ? 0 : servicePosList.size())
+ ",本地缓存数=" + mLocalPosCache.size());
+ ",本地缓存数=" + mLocalPosCache.size());
} catch (Exception e) {
LogUtils.d(TAG, "同步服务数据失败:" + e.getMessage());
@@ -183,9 +210,9 @@ public class LocationActivity extends Activity {
// 1. 多重安全校验(避免销毁后初始化/重复初始化/依赖未就绪)
if (isAdapterInited.get() || !isServiceBound.get() || mMainService == null || mRvPosition == null) {
LogUtils.w(TAG, "Adapter初始化跳过"
+ "已初始化=" + isAdapterInited.get()
+ ",服务绑定=" + isServiceBound.get()
+ ",视图就绪=" + (mRvPosition != null));
+ "已初始化=" + isAdapterInited.get()
+ ",服务绑定=" + isServiceBound.get()
+ ",视图就绪=" + (mRvPosition != null));
return;
}
@@ -195,54 +222,54 @@ public class LocationActivity extends Activity {
// 3. 设置删除回调(删除时同步服务+本地缓存+Adapter
mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
@Override
public void onDeleteClick(int position) {
// 安全校验(索引有效+服务绑定+缓存非空)
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + "");
return;
}
@Override
public void onDeleteClick(int position) {
// 安全校验(索引有效+服务绑定+缓存非空)
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + "");
return;
}
PositionModel deletePos = mLocalPosCache.get(position);
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
// 步骤1调用服务删除确保服务数据一致性
mMainService.removePosition(deletePos.getPositionId());
// 步骤2删除本地缓存确保缓存与服务同步
synchronized (mLocalPosCache) {
mLocalPosCache.remove(position);
}
// 步骤3通知Adapter刷新基于缓存操作避免空数据
mPositionAdapter.notifyItemRemoved(position);
showToast("删除位置成功:" + deletePos.getMemo());
LogUtils.d(TAG, "删除位置完成ID=" + deletePos.getPositionId() + "(服务+缓存已同步)");
}
}
});
PositionModel deletePos = mLocalPosCache.get(position);
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
// 步骤1调用服务删除确保服务数据一致性
mMainService.removePosition(deletePos.getPositionId());
// 步骤2删除本地缓存确保缓存与服务同步
synchronized (mLocalPosCache) {
mLocalPosCache.remove(position);
}
// 步骤3通知Adapter刷新基于缓存操作避免空数据
mPositionAdapter.notifyItemRemoved(position);
showToast("删除位置成功:" + deletePos.getMemo());
LogUtils.d(TAG, "删除位置完成ID=" + deletePos.getPositionId() + "(服务+缓存已同步)");
}
}
});
// 4. 设置保存回调(保存时同步服务+本地缓存+Adapter
mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
@Override
public void onSavePositionClick(int position, PositionModel updatedPos) {
// 安全校验(索引有效+服务绑定+数据非空)
if (!isServiceBound.get() || mMainService == null
|| position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
LogUtils.w(TAG, "保存位置失败:服务未就绪/索引无效/数据空");
showToast("服务未就绪,保存失败");
return;
}
@Override
public void onSavePositionClick(int position, PositionModel updatedPos) {
// 安全校验(索引有效+服务绑定+数据非空)
if (!isServiceBound.get() || mMainService == null
|| position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
LogUtils.w(TAG, "保存位置失败:服务未就绪/索引无效/数据空");
showToast("服务未就绪,保存失败");
return;
}
// 步骤1调用服务更新确保服务数据一致性
mMainService.updatePosition(updatedPos);
// 步骤2更新本地缓存确保缓存与服务同步
synchronized (mLocalPosCache) {
mLocalPosCache.set(position, updatedPos);
}
// 步骤3通知Adapter刷新基于缓存操作避免空数据
mPositionAdapter.notifyItemChanged(position);
showToast("保存位置成功:" + updatedPos.getMemo());
LogUtils.d(TAG, "保存位置完成ID=" + updatedPos.getPositionId() + "(服务+缓存已同步)");
}
});
// 步骤1调用服务更新确保服务数据一致性
mMainService.updatePosition(updatedPos);
// 步骤2更新本地缓存确保缓存与服务同步
synchronized (mLocalPosCache) {
mLocalPosCache.set(position, updatedPos);
}
// 步骤3通知Adapter刷新基于缓存操作避免空数据
mPositionAdapter.notifyItemChanged(position);
showToast("保存位置成功:" + updatedPos.getMemo());
LogUtils.d(TAG, "保存位置完成ID=" + updatedPos.getPositionId() + "(服务+缓存已同步)");
}
});
// 5. 设置Adapter到RecyclerView最后一步确保Adapter已配置完成
mRvPosition.setAdapter(mPositionAdapter);
@@ -268,7 +295,7 @@ public class LocationActivity extends Activity {
}
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
}
// ---------------------- 页面交互新增位置逻辑保留适配GPS数据 ----------------------
/**
* 新增位置调用服务addPosition()可选用当前GPS位置初始化新位置
@@ -395,9 +422,7 @@ public class LocationActivity extends Activity {
LogUtils.d(TAG, "onResume服务已绑定但Adapter未初始化重新同步数据");
syncDataFromMainService();
initPositionAdapter();
}
// 2. 服务已绑定且Adapter已初始化刷新数据确保与服务同步
else if (isServiceBound.get() && mMainService != null && isAdapterInited.get() && mPositionAdapter != null) {
} else if (isServiceBound.get() && mMainService != null && isAdapterInited.get() && mPositionAdapter != null) {
syncDataFromMainService();
mPositionAdapter.notifyDataSetChanged();
LogUtils.d(TAG, "onResume刷新位置数据与服务同步");

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.activities;
package cc.winboll.studio.positions.activities;
import android.app.Activity;
import android.os.Bundle;
@@ -6,17 +6,17 @@ import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.R;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/27 14:26
* @Describe 应用设置窗口
* @Date 2025/12/07 23:29
* @Describe 应用设置活动窗口
*/
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
public static final String TAG = "SettingsActivity";
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
private Toolbar mToolbar;

View File

@@ -1,61 +0,0 @@
package cc.winboll.studio.positions.activities;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.PersistableBundle;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.R;
import cc.winboll.studio.positions.utils.APPPlusUtils;
import cc.winboll.studio.positions.utils.AppConfigsUtil;
import cc.winboll.studio.positions.AppLevel;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/15 13:45
* @Describe 应用快捷方式活动类
*/
public class ShortcutActionActivity extends Activity {
public static final String TAG = "ShortcutActionActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 处理应用级别的切换请求
handleSwitchRequest();
finish();
}
// @Override
// public void onPostCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
// super.onPostCreate(savedInstanceState, persistentState);
// finish();
// }
// @Override
// protected void onStart() {
// super.onStart();
// }
/**
* 处理应用图标快捷菜单的请求
*/
private void handleSwitchRequest() {
Intent intent = getIntent();
if (intent != null && "open_appplus".equals(intent.getDataString())) {
ToastUtils.show("已添加" + getString(R.string.app_name) + "附加组件");
AppConfigsUtil.getInstance(getApplicationContext()).setAppLevel(AppLevel.LAOJUN);
APPPlusUtils.openAPPPlus(this);
//moveTaskToBack(true);
}
if (intent != null && "close_appplus".equals(intent.getDataString())) {
ToastUtils.show("已移除" + getString(R.string.app_name) + "附加组件");
AppConfigsUtil.getInstance(getApplicationContext()).setAppLevel(AppLevel.WUKONG);
APPPlusUtils.closeAPPPlus(this);
//moveTaskToBack(true);
}
}
}

View File

@@ -10,20 +10,16 @@ import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.App;
import cc.winboll.studio.positions.PointLevel;
import cc.winboll.studio.positions.R;
import cc.winboll.studio.positions.utils.ActivityAliasUtils;
import cc.winboll.studio.positions.utils.AppConfigsUtil;
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "WinBoLLActivity";
public static volatile PointLevel _mPointLevel = PointLevel.WUKONG;
protected volatile AESThemeBean.ThemeType mThemeType;
@Override
public Activity getActivity() {
@@ -35,58 +31,25 @@ public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivi
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
mThemeType = getThemeType();
setThemeStyle();
super.onCreate(savedInstanceState);
}
AESThemeBean.ThemeType getThemeType() {
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
}
void setThemeStyle() {
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
}
@Override
protected void onResume() {
super.onResume();
//ToastUtils.show("onResume");
// ActivityAliasUtils 工具使用示例
//
// // 获取真实的目标组件名(即使通过 alias 启动,也能拿到 OriginalActivity
// String realTargetName = ActivityAliasUtils.getRealTargetNameFromIntent(this);
// LogUtils.d("AliasActivity", "真实组件名:" + realTargetName);
// 获取真实的目标组件名(即使通过 alias 启动,也能拿到 OriginalActivity
// String realTargetName = ActivityAliasUtils.getRealTargetNameFromIntent(this);
// LogUtils.d(TAG, "真实组件名:" + realTargetName);
// ToastUtils.show(realTargetName);
// // 判断某个组件是否为 alias
// String componentName = "com.winboll.app.AliasActivity";
// boolean isAlias = ActivityAliasUtils.isActivityAlias(getApplicationContext(), componentName);
// LogUtils.d("判断结果", componentName + " 是否为 alias" + isAlias); // true
// // 获取启动当前 Activity 的组件名(兼容 alias 场景)
// String launchComponent = ActivityAliasUtils.getLaunchComponentName(this);
// LogUtils.d("MainActivity", "启动组件名:" + launchComponent);
/*
* 应用入口逻辑模块
*/
//
// 检查当前活动的启动组件名,设置应用入口级别。
String launchComponent = ActivityAliasUtils.getLaunchComponentName(this);
LogUtils.d("MainActivity", "启动组件名:" + launchComponent);
ToastUtils.show(launchComponent);
// 当前应用处于活动暂停的状态时,就检查应用的入口组件名称,设置应用入口级别。
if (WinBoLLActivity._mPointLevel == PointLevel.DORAEMON) {
if (launchComponent.equals(App.COMPONENT_WUKONG)) {
getSupportActionBar().setTitle(getString(R.string.appplus_name));
ToastUtils.show("WUKONG");
_mPointLevel = PointLevel.WUKONG;
} else if (launchComponent.equals(App.COMPONENT_LAOJUN)) {
getSupportActionBar().setTitle(getString(R.string.app_name));
ToastUtils.show("LAOJUN");
_mPointLevel = PointLevel.LAOJUN;
} else {
// 如果是其他应用组件入口,就关闭活动
finish();
}
}
/*
* 应用级别设置模块
*/
// 读取并配置应用级别
App._mAppLevel = AppConfigsUtil.getInstance(getApplicationContext()).getAppLevel(true);
LogUtils.d(TAG, String.format("onResume %s", getTag()));
}

View File

@@ -9,14 +9,12 @@ package cc.winboll.studio.positions.models;
import android.util.JsonWriter;
import android.util.JsonReader;
import java.io.IOException;
import cc.winboll.studio.positions.AppLevel;
public class AppConfigsModel extends BaseBean {
public static final String TAG = "AppConfigsModel";
boolean isEnableMainService;
AppLevel appLevel;
public AppConfigsModel(boolean isEnableMainService) {
this.isEnableMainService = isEnableMainService;
@@ -26,14 +24,6 @@ public class AppConfigsModel extends BaseBean {
this.isEnableMainService = false;
}
public void setAppLevel(AppLevel appLevel) {
this.appLevel = appLevel;
}
public AppLevel getAppLevel() {
return appLevel;
}
public void setIsEnableMainService(boolean isEnableMainService) {
this.isEnableMainService = isEnableMainService;
}
@@ -52,7 +42,6 @@ public class AppConfigsModel extends BaseBean {
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name("isEnableDistanceRefreshService").value(isEnableMainService());
jsonWriter.name("appLevel").value(getAppLevel().ordinal());
}
// JSON反序列化加载位置数据校验字段
@@ -63,8 +52,6 @@ public class AppConfigsModel extends BaseBean {
} else {
if (name.equals("isEnableDistanceRefreshService")) {
setIsEnableMainService(jsonReader.nextBoolean());
} else if (name.equals("appLevel")) {
setAppLevel((AppLevel.values()[jsonReader.nextInt()]));
} else {
return false;
}

View File

@@ -1,361 +0,0 @@
package cc.winboll.studio.positions.receivers;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/28 19:07
* @Describe MotionStatusReceiver
*/
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.services.MainService;
import cc.winboll.studio.positions.utils.ServiceUtil;
/**
* 运动状态监听Receiver
* 功能1.持续监听传感器(不关闭) 2.每5秒计算运动状态 3.按状态切换GPS模式实时/30秒定时
*/
public class MotionStatusReceiver extends BroadcastReceiver implements SensorEventListener {
public static final String TAG = "MotionStatusReceiver";
// 广播Action
public static final String ACTION_MOTION_STATUS_RECEIVER = "cc.winboll.studio.positions.receivers.MotionStatusReceiver";
public static final String EXTRA_SENSORS_ENABLE = "EXTRA_SENSORS_ENABLE";
// 传感器启动状态标志位
boolean mIsSensorsEnable = false;
// 运动状态常量
private static final int MOTION_STATUS_STATIC = 0; // 静止/低运动
private static final int MOTION_STATUS_WALKING = 1; // 行走/高速运动
// 配置参数(按需求调整)
private static final float ACCELEROMETER_THRESHOLD = 0.8f; // 加速度阈值
private static final float GYROSCOPE_THRESHOLD = 0.5f; // 陀螺仪阈值
private static final long STATUS_CALC_INTERVAL = 5000; // 运动状态计算间隔5秒
private static final long GPS_STATIC_INTERVAL = 30; // 静止时GPS间隔30秒
// 核心对象
private volatile SensorManager mSensorManager;
private Sensor mAccelerometer;
private Sensor mGyroscope;
private volatile boolean mIsSensorListening = false; // 传感器是否持续监听
private int mCurrentMotionStatus = MOTION_STATUS_STATIC; // 当前运动状态
private Handler mMainHandler; // 主线程Handler用于定时计算
private Context mBroadcastContext; // 广播上下文
// 传感器数据缓存用于5秒内数据汇总避免单次波动误判
private float mAccelMax = 0f; // 5秒内加速度最大值
private float mGyroMax = 0f; // 5秒内陀螺仪最大值
@Override
public void onReceive(Context context, Intent intent) {
LogUtils.d(TAG, "===== 接收器启动onReceive() 开始执行 =====");
this.mBroadcastContext = context;
mMainHandler = new Handler(Looper.getMainLooper());
if (TextUtils.equals(intent.getAction(), ACTION_MOTION_STATUS_RECEIVER)) {
boolean isSettingEnable = intent.getBooleanExtra(EXTRA_SENSORS_ENABLE, false);
if (mIsSensorsEnable == false && isSettingEnable == true) {
mIsSensorsEnable = true;
// 1. 初始化传感器(必执行)
initSensors();
if (mAccelerometer == null || mGyroscope == null) {
LogUtils.e(TAG, "设备缺少加速度/陀螺仪,无法持续监听");
cleanResources(false); // 传感器不可用才清理
return;
}
// 2. 校验参数
if (context == null || intent == null) {
LogUtils.d(TAG, "onReceive():无效参数,终止处理");
cleanResources(false);
return;
}
LogUtils.d(TAG, "onReceive()接收到广播Action=" + intent.getAction());
// 3. 启动持续传感器监听(核心:不关闭,重复调用无影响)
startSensorListening();
// 4. 启动5秒定时计算运动状态核心持续触发状态判断
startStatusCalcTimer();
}
}
// 5. 处理外部广播触发(可选,保留外部控制能力)
// if (TextUtils.equals(intent.getAction(), ACTION_MOTION_STATUS_RECEIVER)) {
// int motionStatus = intent.getIntExtra(EXTRA_MOTION_STATUS, MOTION_STATUS_STATIC);
// String statusDesc = motionStatus == MOTION_STATUS_WALKING ? "高速运动" : "静止/低运动";
// LogUtils.d(TAG, "外部广播触发,强制设置运动状态:" + statusDesc);
// mCurrentMotionStatus = motionStatus;
// handleMotionStatus(mCurrentMotionStatus); // 立即执行GPS切换
// }
}
/**
* 初始化传感器(持续监听,复用实例)
*/
private void initSensors() {
LogUtils.d(TAG, "initSensors():初始化传感器");
if (mSensorManager != null || mBroadcastContext == null) return;
mSensorManager = (SensorManager) mBroadcastContext.getSystemService(Context.SENSOR_SERVICE);
if (mSensorManager == null) {
LogUtils.e(TAG, "设备不支持传感器服务");
return;
}
// 获取传感器实例(持续复用,不销毁)
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
LogUtils.d(TAG, "传感器初始化结果:加速度=" + (mAccelerometer != null) + ",陀螺仪=" + (mGyroscope != null));
}
/**
* 启动传感器持续监听(核心:不关闭,注册一次一直生效)
*/
private void startSensorListening() {
if (mSensorManager == null || mAccelerometer == null || mGyroscope == null) return;
if (!mIsSensorListening) {
// 注册传感器监听(持续生效,直到服务销毁才注销)
mSensorManager.registerListener(
this,
mAccelerometer,
SensorManager.SENSOR_DELAY_NORMAL, // 正常延迟,平衡性能与精度
mMainHandler
);
mSensorManager.registerListener(
this,
mGyroscope,
SensorManager.SENSOR_DELAY_NORMAL,
mMainHandler
);
mIsSensorListening = true;
LogUtils.d(TAG, "startSensorListening():传感器持续监听已启动(不关闭)");
}
}
/**
* 启动5秒定时计算运动状态核心周期性汇总传感器数据
*/
private void startStatusCalcTimer() {
if (mMainHandler == null) return;
// 移除旧任务(避免重复注册)
mMainHandler.removeCallbacks(mStatusCalcRunnable);
// 启动定时任务每5秒执行一次
mMainHandler.postDelayed(mStatusCalcRunnable, STATUS_CALC_INTERVAL);
LogUtils.d(TAG, "startStatusCalcTimer()5秒运动状态计算定时器已启动");
}
/**
* 运动状态计算任务5秒执行一次
*/
private final Runnable mStatusCalcRunnable = new Runnable() {
@Override
public void run() {
// 1. 基于5秒内缓存的最大传感器数据判断状态
boolean isHighMotion = (mAccelMax > ACCELEROMETER_THRESHOLD) && (mGyroMax > GYROSCOPE_THRESHOLD);
int newMotionStatus = isHighMotion ? MOTION_STATUS_WALKING : MOTION_STATUS_STATIC;
// 2. 状态变化时才处理避免频繁切换GPS
if (newMotionStatus != mCurrentMotionStatus) {
mCurrentMotionStatus = newMotionStatus;
String statusDesc = isHighMotion ? "高速运动" : "静止/低运动";
LogUtils.d(TAG, "运动状态更新5秒计算" + statusDesc
+ "(加速度最大值=" + mAccelMax + ",陀螺仪最大值=" + mGyroMax + "");
handleMotionStatus(newMotionStatus); // 切换GPS模式
} else {
LogUtils.d(TAG, "运动状态无变化5秒计算" + (isHighMotion ? "高速运动" : "静止/低运动"));
}
// 3. 重置传感器数据缓存准备下一个5秒周期
mAccelMax = 0f;
mGyroMax = 0f;
// 4. 循环执行定时任务(核心:持续计算)
mMainHandler.postDelayed(this, STATUS_CALC_INTERVAL);
}
};
/**
* 传感器数据变化回调(核心:实时缓存最大数据)
*/
@Override
public void onSensorChanged(SensorEvent event) {
if (event == null) return;
// 实时缓存5秒内的最大传感器数据避免单次波动误判
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
float accelTotal = Math.abs(event.values[0]) + Math.abs(event.values[1]) + Math.abs(event.values[2]);
if (accelTotal > mAccelMax) mAccelMax = accelTotal; // 缓存最大值
LogUtils.d(TAG, "加速度传感器实时数据:合值=" + accelTotal + "当前5秒最大值=" + mAccelMax + "");
break;
case Sensor.TYPE_GYROSCOPE:
float gyroTotal = Math.abs(event.values[0]) + Math.abs(event.values[1]) + Math.abs(event.values[2]);
if (gyroTotal > mGyroMax) mGyroMax = gyroTotal; // 缓存最大值
LogUtils.d(TAG, "陀螺仪实时数据:合值=" + gyroTotal + "当前5秒最大值=" + mGyroMax + "");
break;
}
}
/**
* 处理运动状态核心按状态切换GPS模式
*/
private void handleMotionStatus(int motionStatus) {
LogUtils.d(TAG, "handleMotionStatus()开始处理运动状态切换GPS模式");
if (mBroadcastContext == null) {
LogUtils.w(TAG, "上下文为空无法处理GPS");
return;
}
MainService mainService = getMainService();
if (mainService == null) {
LogUtils.e(TAG, "MainService未启动GPS控制失败");
return;
}
if (motionStatus == MOTION_STATUS_WALKING) {
// 高速运动启动GPS实时更新2秒/1米
handleHighMotionGPS(mainService);
} else {
// 静止/低运动启动GPS30秒定时更新
handleStaticGPS(mainService);
}
}
/**
* 高速运动GPS处理实时更新
*/
private void handleHighMotionGPS(MainService mainService) {
// 动态权限判断Android 6.0+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
mBroadcastContext.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
sendPermissionRequestBroadcast();
return;
}
// 启动实时GPS已启动则不重复操作
if (!mainService.isGpsListening()) {
mainService.startGpsLocation(); // 实时更新2秒/1米
mainService.stopGpsStaticTimer(); // 停止定时GPS
LogUtils.d(TAG, "高速运动已启动GPS实时更新");
}
}
/**
* 静止/低运动GPS处理30秒定时更新
*/
private void handleStaticGPS(MainService mainService) {
// 停止实时GPS已停止则不重复操作
if (mainService.isGpsListening()) {
mainService.stopGpsLocation(); // 停止实时更新
LogUtils.d(TAG, "静止/低运动已停止GPS实时更新");
}
// 启动30秒定时GPS已启动则不重复操作
mainService.startGpsStaticTimer(GPS_STATIC_INTERVAL); // 30秒一次
LogUtils.d(TAG, "静止/低运动已启动GPS30秒定时更新");
}
/**
* 获取MainService实例复用逻辑
*/
private MainService getMainService() {
if (mBroadcastContext == null) return null;
// 优先获取单例
MainService singleton = MainService.getInstance(mBroadcastContext);
if (singleton != null && singleton.isServiceRunning()) {
return singleton;
}
// 启动服务并重试
if (!ServiceUtil.isServiceAlive(mBroadcastContext, MainService.class.getName())) {
mBroadcastContext.startService(new Intent(mBroadcastContext, MainService.class));
try {
Thread.sleep(500); // 等待服务启动
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return MainService.getInstance(mBroadcastContext);
}
/**
* 发送GPS权限申请广播Receiver无法直接申请
*/
private void sendPermissionRequestBroadcast() {
Intent permissionIntent = new Intent("cc.winboll.studio.positions.ACTION_REQUEST_GPS_PERMISSION");
permissionIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
mBroadcastContext.sendBroadcast(permissionIntent);
LogUtils.d(TAG, "GPS权限缺失已发送申请广播");
}
/**
* 资源清理核心传感器不关闭仅清理Handler和上下文
* @param isForceStopSensor 是否强制停止传感器仅服务销毁时传true
*/
private void cleanResources(boolean isForceStopSensor) {
// 1. 停止定时计算任务
if (mMainHandler != null) {
mMainHandler.removeCallbacksAndMessages(null);
mMainHandler = null;
LogUtils.d(TAG, "cleanResources():已停止运动状态计算定时器");
}
// 2. 强制停止传感器(仅当外部触发销毁时执行,正常情况不关闭)
if (isForceStopSensor && mSensorManager != null && mIsSensorListening) {
mSensorManager.unregisterListener(this);
mIsSensorListening = false;
LogUtils.d(TAG, "cleanResources():已强制停止传感器监听");
}
// 3. 置空上下文(避免内存泄漏)
mBroadcastContext = null;
}
/**
* 传感器精度变化回调(日志监控)
*/
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
String sensorType = sensor.getType() == Sensor.TYPE_ACCELEROMETER ? "加速度" : "陀螺仪";
String accuracyDesc = getAccuracyDesc(accuracy);
LogUtils.d(TAG, sensorType + "传感器精度变化:" + accuracyDesc);
}
/**
* 传感器精度描述转换
*/
private String getAccuracyDesc(int accuracy) {
switch (accuracy) {
case SensorManager.SENSOR_STATUS_ACCURACY_HIGH: return "";
case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM: return "";
case SensorManager.SENSOR_STATUS_ACCURACY_LOW: return "";
case SensorManager.SENSOR_STATUS_UNRELIABLE: return "不可靠";
default: return "未知";
}
}
/**
* 补充Receiver销毁时强制清理需在MainService注销时调用
*/
public void forceCleanResources() {
cleanResources(true); // 强制停止传感器
}
}

View File

@@ -1,194 +0,0 @@
package cc.winboll.studio.positions.utils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/10 09:51
* @Describe 应用图标切换工具类(启用组件时创建对应快捷方式)
*/
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.positions.App;
import cc.winboll.studio.positions.MainActivity;
public class APPPlusUtils {
public static final String TAG = "APPPlusUtils";
// 快捷方式配置(名称+图标,需与实际资源匹配)
// private static final String PLUS_SHORTCUT_NAME = "位置服务-Laojun";
// private static final int PLUS_SHORTCUT_ICON = R.mipmap.ic_launcher; // Laojun 图标资源
/**
* 添加Plus组件与图标
*/
public static boolean openAPPPlus(Context context) {
if (context == null) {
LogUtils.d(TAG, "切换失败:上下文为空");
Toast.makeText(context, "图标切换失败", Toast.LENGTH_SHORT).show();
return false;
}
PackageManager pm = context.getPackageManager();
ComponentName plusComponentLaojun = new ComponentName(context, App.COMPONENT_LAOJUN);
//ComponentName plusComponentWuKong = new ComponentName(context, MainActivity.COMPONENT_WUKONG);
try {
//disableComponent(pm, plusComponentWuKong);
enableComponent(pm, plusComponentLaojun);
// 2. 创建 Laojun 组件对应的快捷方式(自动去重)
// boolean shortcutCreated = createComponentShortcut(context, plusComponent, PLUS_SHORTCUT_NAME, PLUS_SHORTCUT_ICON);
//
// // 3. 通知桌面刷新图标
// context.sendBroadcast(new Intent(Intent.ACTION_PACKAGE_CHANGED)
// .setData(android.net.Uri.parse("package:" + context.getPackageName())));
//
// // 4. 反馈结果
// String logMsg = shortcutCreated ? "启用 Laojun + 快捷方式创建成功" : "启用 Laojun 成功,快捷方式创建失败";
// String toastMsg = shortcutCreated ? "图标切换为 Laojun已创建快捷方式" : "图标切换为 Laojun快捷方式创建失败";
// LogUtils.d(TAG, logMsg);
// Toast.makeText(context, toastMsg, Toast.LENGTH_SHORT).show();
//
return true;
} catch (Exception e) {
LogUtils.e(TAG, "Laojun 图标切换失败:" + e.getMessage());
// 失败兜底:启用 Wukong 组件
//enableComponent(pm, wukongComponent);
Toast.makeText(context, "图标切换失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* 移除Plus组件
*/
public static boolean closeAPPPlus(Context context) {
if (context == null) {
LogUtils.d(TAG, "切换失败:上下文为空");
Toast.makeText(context, "图标切换失败", Toast.LENGTH_SHORT).show();
return false;
}
PackageManager pm = context.getPackageManager();
ComponentName plusComponentLaojun = new ComponentName(context, App.COMPONENT_LAOJUN);
//ComponentName plusComponentWuKong = new ComponentName(context, MainActivity.COMPONENT_WUKONG);
disableComponent(pm, plusComponentLaojun);
//enableComponent(pm, plusComponentWuKong);
return true;
}
/**
* 创建指定组件的桌面快捷方式(自动去重,兼容 Android 8.0+
* @param component 目标组件(如 LAOJUN_ACTIVITY
* @param name 快捷方式名称
* @param iconRes 快捷方式图标资源ID
* @return 是否创建成功
*/
private static boolean createComponentShortcut(Context context, ComponentName component, String name, int iconRes) {
if (context == null || component == null || name == null || iconRes == 0) {
LogUtils.d(TAG, "快捷方式创建失败:参数为空");
return false;
}
// Android 8.0+API 26+):使用 ShortcutManager系统推荐
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
PackageManager pm = context.getPackageManager();
android.content.pm.ShortcutManager shortcutManager = context.getSystemService(android.content.pm.ShortcutManager.class);
if (shortcutManager == null || !shortcutManager.isRequestPinShortcutSupported()) {
LogUtils.d(TAG, "系统不支持创建快捷方式");
return false;
}
// 检查是否已存在该组件的快捷方式(去重)
for (android.content.pm.ShortcutInfo info : shortcutManager.getPinnedShortcuts()) {
if (component.getClassName().equals(info.getIntent().getComponent().getClassName())) {
LogUtils.d(TAG, "快捷方式已存在:" + component.getClassName());
return true;
}
}
// 构建启动目标组件的意图
Intent launchIntent = new Intent(Intent.ACTION_MAIN)
.setComponent(component)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 构建快捷方式信息
android.content.pm.ShortcutInfo shortcutInfo = new android.content.pm.ShortcutInfo.Builder(context, component.getClassName())
.setShortLabel(name)
.setLongLabel(name)
.setIcon(android.graphics.drawable.Icon.createWithResource(context, iconRes))
.setIntent(launchIntent)
.build();
// 请求创建快捷方式(需用户确认)
shortcutManager.requestPinShortcut(shortcutInfo, null);
return true;
} catch (Exception e) {
LogUtils.d(TAG, "Android O+ 快捷方式创建失败:" + e.getMessage());
return false;
}
} else {
// Android 8.0 以下:使用广播(兼容旧机型)
try {
// 构建启动目标组件的意图
Intent launchIntent = new Intent(Intent.ACTION_MAIN)
.setComponent(component)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// 构建创建快捷方式的广播意图
Intent installIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
installIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent);
installIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(context, iconRes));
installIntent.putExtra("duplicate", false); // 禁止重复创建
context.sendBroadcast(installIntent);
return true;
} catch (Exception e) {
LogUtils.d(TAG, "Android O- 快捷方式创建失败:" + e.getMessage());
return false;
}
}
}
/**
* 启用组件(带状态检查,避免重复操作)
*/
private static void enableComponent(PackageManager pm, ComponentName component) {
if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
);
}
}
/**
* 禁用组件(带状态检查,避免重复操作)
*/
private static void disableComponent(PackageManager pm, ComponentName component) {
if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
);
}
}
}

View File

@@ -1,148 +0,0 @@
package cc.winboll.studio.positions.utils;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/15 15:23
* @Describe Activity Alias 工具类(兼容 Android 所有版本Java 7 语法)
* 用于获取 activity-alias 对应的原始 Activity 组件名、判断 alias 类型、获取启动组件名等
*/
public class ActivityAliasUtils {
private static final String TAG = "ActivityAliasUtils";
/**
* 获取 activity-alias 指向的原始 Activity 组件名
*
* @param context 上下文(建议用 ApplicationContext
* @param aliasName activity-alias 的组件名(完整路径,如 ".AliasActivity" 或 "com.winboll.app.AliasActivity"
* @return 原始 Activity 的完整组件名(如 "com.winboll.app.OriginalActivity"),失败返回 null
*/
public static String getTargetActivityName(Context context, String aliasName) {
// 校验参数
if (context == null || TextUtils.isEmpty(aliasName)) {
LogUtils.e(TAG, "getTargetActivityName: context is null or aliasName is empty");
return null;
}
// 补全组件名(若传入的是短名,自动拼接包名)
String fullAliasName = aliasName.startsWith(".")
? context.getPackageName() + aliasName
: aliasName;
try {
// 1. 获取 PackageManager
PackageManager packageManager = context.getPackageManager();
// 2. 解析 activity-alias 的 ActivityInfoflag 必须设为 PackageManager.GET_META_DATA否则可能获取不到 targetActivity
ActivityInfo aliasActivityInfo = packageManager.getActivityInfo(
new android.content.ComponentName(context.getPackageName(), fullAliasName),
PackageManager.GET_META_DATA
);
// 3. 获取 targetActivity原始 Activity 组件名)
String targetActivity = aliasActivityInfo.targetActivity;
if (TextUtils.isEmpty(targetActivity)) {
LogUtils.e(TAG, "getTargetActivityName: targetActivity is empty for alias " + fullAliasName);
return null;
}
// 4. 补全原始 Activity 的完整包名(若 targetActivity 是短名)
String fullTargetName = targetActivity.startsWith(".")
? context.getPackageName() + targetActivity
: targetActivity;
LogUtils.d(TAG, "getTargetActivityName: alias=" + fullAliasName + ", target=" + fullTargetName);
return fullTargetName;
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getTargetActivityName: alias not found - " + fullAliasName, e);
} catch (Exception e) {
LogUtils.e(TAG, "getTargetActivityName: unknown error", e);
}
return null;
}
/**
* 判断某个组件名是否为 activity-alias而非原始 Activity
*
* @param context 上下文
* @param componentName 待判断的组件名(完整路径)
* @return true是 activity-aliasfalse不是或判断失败
*/
public static boolean isActivityAlias(Context context, String componentName) {
// 调用 getTargetActivityName若返回非空则说明是 alias
return !TextUtils.isEmpty(getTargetActivityName(context, componentName));
}
/**
* 从启动的 Intent 中获取实际的目标组件名(处理 alias 场景)
* 适用于 Activity 中获取自身真实组件名(原始 Activity 名)
*
* @param context 当前 Activity 上下文
* @return 真实的目标组件名(原始 Activity 名,若为 alias 启动则返回原始 Activity否则返回自身
*/
public static String getRealTargetNameFromIntent(Context context) {
if (context == null) {
LogUtils.e(TAG, "getRealTargetNameFromIntent: context is null");
return null;
}
// 获取当前 Activity 的组件名(可能是 alias
String currentComponentName = context.getClass().getName();
// 检查是否为 alias若是则返回 target否则返回自身
String targetName = getTargetActivityName(context, currentComponentName);
return TextUtils.isEmpty(targetName) ? currentComponentName : targetName;
}
/**
* 获取当前活动上下文Activity的启动组件名即启动时使用的组件名可能是 alias 或原始 Activity
* 场景:若通过 alias 启动 Activity返回 alias 名;若直接启动原始 Activity返回原始 Activity 名
*
* @param context 当前 Activity 上下文(必须是 Activity 实例,不能是 ApplicationContext
* @return 启动组件的完整名,失败返回 null
*/
public static String getLaunchComponentName(Context context) {
// 1. 校验上下文类型(必须是 Activity否则无法获取启动 Intent
if (context == null) {
LogUtils.e(TAG, "getLaunchComponentName: context is null");
return null;
}
if (!(context instanceof android.app.Activity)) {
LogUtils.e(TAG, "getLaunchComponentName: context must be Activity instance, current is " + context.getClass().getName());
return null;
}
try {
// 2. 获取启动当前 Activity 的 Intent
android.app.Activity activity = (android.app.Activity) context;
Intent launchIntent = activity.getIntent();
if (launchIntent == null) {
LogUtils.e(TAG, "getLaunchComponentName: launch Intent is null");
return null;
}
// 3. 从 Intent 中获取启动组件名ComponentName
android.content.ComponentName componentName = launchIntent.getComponent();
if (componentName == null) {
LogUtils.e(TAG, "getLaunchComponentName: ComponentName is null in launch Intent");
return null;
}
// 4. 获取组件的完整类名(即启动时使用的组件名)
String launchComponentName = componentName.getClassName();
LogUtils.d(TAG, "getLaunchComponentName: current launch component is " + launchComponentName);
return launchComponentName;
} catch (Exception e) {
LogUtils.e(TAG, "getLaunchComponentName: failed to get launch component name", e);
return null;
}
}
}

View File

@@ -1,8 +1,6 @@
package cc.winboll.studio.positions.utils;
import android.content.Context;
import cc.winboll.studio.positions.AppLevel;
import cc.winboll.studio.positions.models.AppConfigsModel;
import cc.winboll.studio.positions.App;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -60,27 +58,11 @@ public class AppConfigsUtil {
}
public void setIsEnableMainService(boolean isEnableMainService) {
if (mAppConfigsModel == null) {
if(mAppConfigsModel == null) {
mAppConfigsModel = new AppConfigsModel();
}
mAppConfigsModel.setIsEnableMainService(isEnableMainService);
saveConfigs();
}
public AppLevel getAppLevel(boolean isReloadConfigs) {
if (isReloadConfigs) {
loadConfigs();
}
return (mAppConfigsModel == null) ?AppLevel.WUKONG: mAppConfigsModel.getAppLevel();
}
public void setAppLevel(AppLevel appLevel) {
if (mAppConfigsModel == null) {
mAppConfigsModel = new AppConfigsModel();
}
App._mAppLevel = appLevel;
mAppConfigsModel.setAppLevel(appLevel);
saveConfigs();
}
}

View File

@@ -1,241 +0,0 @@
package cc.winboll.studio.positions.utils;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/13 15:42
* @Describe JsonShareHandler
* 外部 JSON 文件分享处理工具类
* 功能:接收外部分享的 .json 文件,弹出确认对话框,保存到外部存储 files/BaseBean 目录
*/
public class JsonShareHandler {
private static final String TAG = "JsonShareHandler";
private static final String TARGET_DIR = "BaseBean";
private static final String MIME_TYPE_JSON = "application/json";
private static final String FILE_SUFFIX_JSON = ".json";
// 对话框回调接口Java7 无 Lambda用接口实现
public interface ConfirmCallback {
void onConfirm(boolean isConfirm);
}
/**
* 处理外部分享的 Intent先弹出确认对话框再决定是否接收文件
* @param context 上下文(需为 Activity否则无法弹出对话框
* @param intent 分享 Intent
* @param callback 确认结果回调(用于 Activity 处理后续逻辑)
*/
public static void handleSharedJsonWithConfirm(final Context context, final Intent intent, final ConfirmCallback callback) {
if (context == null || intent == null || callback == null) {
Log.e(TAG, "参数为空,处理失败");
if (callback != null) callback.onConfirm(false);
return;
}
// 1. 先验证 Intent 合法性(提前过滤无效分享)
String action = intent.getAction();
String type = intent.getType();
if (!Intent.ACTION_SEND.equals(action) || type == null) {
Log.e(TAG, "非文件分享 Intent");
Toast.makeText(context, "不支持的分享类型", Toast.LENGTH_SHORT).show();
callback.onConfirm(false);
return;
}
// 2. 弹出确认对话框
new AlertDialog.Builder(context)
.setTitle("接收 JSON 文件")
.setMessage("是否接收并保存该 JSON 文件?")
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
// 3. 点击 Yes处理文件保存
String savedPath = handleSharedJsonFile(context, intent);
if (savedPath != null) {
Toast.makeText(context, "文件保存成功:" + savedPath, Toast.LENGTH_LONG).show();
callback.onConfirm(true);
} else {
Toast.makeText(context, "文件保存失败", Toast.LENGTH_SHORT).show();
callback.onConfirm(false);
}
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
// 4. 点击 No直接退出处理
callback.onConfirm(false);
}
})
.setCancelable(false) // 不可点击外部取消
.show();
}
/**
* 核心文件处理逻辑(原有功能,无修改)
*/
private static String handleSharedJsonFile(Context context, Intent intent) {
String action = intent.getAction();
String type = intent.getType();
// 验证 JSON 格式
if (!MIME_TYPE_JSON.equals(type) && !type.contains("json")) {
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri == null || !getFileNameFromUri(context, uri).endsWith(FILE_SUFFIX_JSON)) {
Log.e(TAG, "接收的文件不是 JSON 格式");
return null;
}
}
Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedUri == null) {
Log.e(TAG, "未获取到分享的文件 Uri");
return null;
}
try {
// 创建保存目录
File saveDir = getTargetSaveDir(context);
if (!saveDir.exists() && !saveDir.mkdirs()) {
Log.e(TAG, "创建保存目录失败:" + saveDir.getAbsolutePath());
return null;
}
// 解析文件名(兼容低版本)
String fileName = getFileNameFromUri(context, sharedUri);
if (fileName == null || !fileName.endsWith(FILE_SUFFIX_JSON)) {
fileName = "default_" + System.currentTimeMillis() + FILE_SUFFIX_JSON;
Log.w(TAG, "文件名解析失败,使用默认名称:" + fileName);
}
// 复制文件
File targetFile = new File(saveDir, fileName);
boolean copySuccess = copyFileFromUri(context, sharedUri, targetFile);
return copySuccess ? targetFile.getAbsolutePath() : null;
} catch (Exception e) {
Log.e(TAG, "处理分享文件异常:" + e.getMessage());
return null;
}
}
/**
* 获取目标保存目录(兼容 Android 10+ 分区存储)
*/
private static File getTargetSaveDir(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return new File(context.getExternalFilesDir(null), TARGET_DIR);
} else {
return new File(
Environment.getExternalStorageDirectory() + File.separator +
"Android" + File.separator +
"data" + File.separator +
context.getPackageName() + File.separator +
"files" + File.separator +
TARGET_DIR
);
}
}
/**
* 从 Uri 解析文件名(兼容所有 Android 版本)
*/
private static String getFileNameFromUri(Context context, Uri uri) {
if (uri == null) return null;
// 1. 文件 Urifile:// 开头)
if ("file".equals(uri.getScheme())) {
return new File(uri.getPath()).getName();
}
// 2. 内容 Uricontent:// 开头)
if ("content".equals(uri.getScheme())) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
uri,
new String[]{MediaStore.MediaColumns.DISPLAY_NAME},
null,
null,
null
);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
if (nameIndex != -1) {
return cursor.getString(nameIndex);
}
}
} catch (Exception e) {
Log.e(TAG, "解析内容 Uri 文件名失败:" + e.getMessage());
} finally {
if (cursor != null) cursor.close();
}
}
// 3. 解析失败,返回默认名称
String lastPathSegment = uri.getLastPathSegment();
return lastPathSegment != null ? lastPathSegment : "unknown.json";
}
/**
* 复制 Uri 指向的文件到目标路径
*/
private static boolean copyFileFromUri(Context context, Uri sourceUri, File targetFile) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = context.getContentResolver().openInputStream(sourceUri);
if (inputStream == null) {
Log.e(TAG, "无法打开源文件输入流");
return false;
}
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[1024 * 4];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
Log.d(TAG, "文件保存成功:" + targetFile.getAbsolutePath());
return true;
} catch (IOException e) {
Log.e(TAG, "文件复制异常:" + e.getMessage());
return false;
} finally {
try {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
} catch (IOException e) {
Log.e(TAG, "关闭流异常:" + e.getMessage());
}
}
}
/**
* 检查外部存储是否可用
*/
public static boolean isExternalStorageAvailable() {
String state = Environment.getExternalStorageState();
return Environment.MEDIA_MOUNTED.equals(state);
}
}

View File

@@ -1,168 +0,0 @@
package cc.winboll.studio.positions.utils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/05 15:49
* @Describe LocalMotionDetector
*/
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 本机运动状态监测工具(无联网,纯传感器)
*/
public class LocalMotionDetector implements SensorEventListener {
public static final String TAG = "LocalMotionDetector";
// 配置参数(重点修改:调高运动阈值,适配坐立持机场景)
private static final float MOTION_THRESHOLD = 1.8f; // 从0.5f调高到1.8f(过滤坐立轻微晃动)
private static final long STATUS_CHECK_INTERVAL = 3000; // 3秒判断一次状态
private static final int STEP_CHANGE_THRESHOLD = 2; // 3秒≥2步判定行走
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private Sensor mStepCounter;
private Handler mMainHandler;
private MotionStatusCallback mCallback;
private boolean mIsDetecting = false;
private float mLastAccelMagnitude = 0f;
private int mLastStepCount = 0;
private int mCurrentStepCount = 0;
private boolean mIsWalking = false;
// 单例模式
private static LocalMotionDetector sInstance;
public static LocalMotionDetector getInstance() {
if (sInstance == null) {
synchronized (LocalMotionDetector.class) {
if (sInstance == null) {
sInstance = new LocalMotionDetector();
}
}
}
return sInstance;
}
private LocalMotionDetector() {
mMainHandler = new Handler(Looper.getMainLooper());
}
/**
* 开始监测运动状态
*/
public void startDetection(Context context, MotionStatusCallback callback) {
if (mIsDetecting) return;
mCallback = callback;
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
// 初始化传感器
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mStepCounter = mSensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
// 注册传感器监听
if (mAccelerometer != null) {
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL, mMainHandler);
}
if (mStepCounter != null) {
mSensorManager.registerListener(this, mStepCounter, SensorManager.SENSOR_DELAY_NORMAL, mMainHandler);
LogUtils.d(TAG, "计步传感器已启动");
} else {
LogUtils.d(TAG, "设备不支持计步传感器,仅用加速度判断");
}
// 启动定时状态检测
mMainHandler.postDelayed(mStatusCheckRunnable, STATUS_CHECK_INTERVAL);
mIsDetecting = true;
LogUtils.d(TAG, "运动状态监测已启动");
}
/**
* 停止监测
*/
public void stopDetection() {
if (!mIsDetecting) return;
if (mSensorManager != null) {
mSensorManager.unregisterListener(this);
}
mMainHandler.removeCallbacksAndMessages(null);
mIsDetecting = false;
mIsWalking = false;
mCallback = null;
LogUtils.d(TAG, "运动状态监测已停止");
}
@Override
public void onSensorChanged(SensorEvent event) {
if (!mIsDetecting) return;
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
// 计算加速度幅度(保留原逻辑,阈值已调高)
float accelX = Math.abs(event.values[0]);
float accelY = Math.abs(event.values[1]);
float accelZ = Math.abs(event.values[2]);
mLastAccelMagnitude = accelX + accelY + accelZ;
break;
case Sensor.TYPE_STEP_COUNTER:
// 累计步数
mCurrentStepCount = (int) event.values[0];
break;
}
}
/**
* 定时判断运动状态优化逻辑计步为0时即使有轻微加速度也判定为静止
*/
private final Runnable mStatusCheckRunnable = new Runnable() {
@Override
public void run() {
if (!mIsDetecting || mCallback == null) return;
//LogUtils.d(TAG, "mStatusCheckRunnable run");
boolean newIsWalking = false;
// 结合计步器+加速度判断(优化:优先计步,无步数时严格按高阈值判断)
if (mStepCounter != null) {
int stepChange = mCurrentStepCount - mLastStepCount;
// 只有“步数达标” 或 “无步数但加速度远超坐立幅度”,才判定为行走
newIsWalking = (stepChange >= STEP_CHANGE_THRESHOLD)
&& (mLastAccelMagnitude >= MOTION_THRESHOLD); // 增加步数+加速度双重校验
mLastStepCount = mCurrentStepCount;
} else {
// 无计步器时,仅用高阈值判断
newIsWalking = mLastAccelMagnitude >= MOTION_THRESHOLD;
}
// 状态变化时回调
if (newIsWalking != mIsWalking) {
mIsWalking = newIsWalking;
String statusDesc = mIsWalking ? "行走状态" : "静止/低运动状态";
LogUtils.d(TAG, "运动状态变化:" + statusDesc + " | 加速度幅度:" + mLastAccelMagnitude); // 增加日志便于调试
mCallback.onMotionStatusChanged(mIsWalking, statusDesc);
}
LogUtils.d(TAG, String.format("运动状态 newIsWalking %s", newIsWalking));
// 循环检测
mMainHandler.postDelayed(this, STATUS_CHECK_INTERVAL);
}
};
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
/**
* 运动状态回调接口
*/
public interface MotionStatusCallback {
void onMotionStatusChanged(boolean isWalking, String statusDesc);
}
}

View File

@@ -1,105 +0,0 @@
package cc.winboll.studio.positions.utils;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.os.Bundle;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.positions.PointLevel;
import cc.winboll.studio.positions.activities.WinBoLLActivity;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/15 15:59
* @Describe 应用活动窗口状态响应类
* 主要用于设置应用级别与组件状态
*/
public class MyActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
public static final String TAG = "MyActivityLifecycleCallbacks";
public String mInfo = "";
public MyActivityLifecycleCallbacks() {
}
void createActivityeInfo(Activity activity) {
StringBuilder sb = new StringBuilder();
Intent receivedIntent = activity.getIntent();
sb.append("\nCallingActivity : \n");
if (activity.getCallingActivity() != null) {
sb.append(activity.getCallingActivity().getPackageName());
}
sb.append("\nReceived Intent Package : \n");
sb.append(receivedIntent.getPackage());
Bundle extras = receivedIntent.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
sb.append("\nIntentInfo");
sb.append("\n键: ");
sb.append(key);
sb.append(", 值: ");
sb.append(extras.get(key));
//Log.d("IntentInfo", "键: " + key + ", 值: " + extras.get(key));
}
}
mInfo = sb.toString();
//Log.d("IntentInfo", "发送Intent的应用包名: " + senderPackage);
}
public void showActivityeInfo() {
//ToastUtils.show("ActivityeInfo : " + mInfo);
LogUtils.d(TAG, "ActivityeInfo : " + mInfo);
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// 在这里可以做一些初始化相关的操作例如记录Activity的创建时间等
//System.out.println(activity.getLocalClassName() + " was created");
LogUtils.d(TAG, activity.getLocalClassName() + " was created");
createActivityeInfo(activity);
}
@Override
public void onActivityStarted(Activity activity) {
//System.out.println(activity.getLocalClassName() + " was started");
LogUtils.d(TAG, activity.getLocalClassName() + " was started");
//createActivityeInfo(activity);
}
@Override
public void onActivityResumed(Activity activity) {
//System.out.println(activity.getLocalClassName() + " was resumed");
LogUtils.d(TAG, activity.getLocalClassName() + " was resumed");
//createActivityeInfo(activity);
}
@Override
public void onActivityPaused(Activity activity) {
ToastUtils.show("Activity Paused");
// 应用从正在活动状态抽离出来时设置应用入口级别状态设置为时空虚幻而不确定的哆啦A梦级别。
WinBoLLActivity._mPointLevel = PointLevel.DORAEMON;
//System.out.println(activity.getLocalClassName() + " was paused");
LogUtils.d(TAG, activity.getLocalClassName() + " was paused");
}
@Override
public void onActivityStopped(Activity activity) {
//System.out.println(activity.getLocalClassName() + " was stopped");
LogUtils.d(TAG, activity.getLocalClassName() + " was stopped");
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
// 可以在这里添加保存状态的自定义逻辑
}
@Override
public void onActivityDestroyed(Activity activity) {
//System.out.println(activity.getLocalClassName() + " was destroyed");
LogUtils.d(TAG, activity.getLocalClassName() + " was destroyed");
}
}

View File

@@ -1,282 +0,0 @@
package cc.winboll.studio.positions.views;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/10 08:29
* @Describe 沙漏计时器控件
*/
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ClipDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.text.InputFilter;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Switch;
import android.widget.TextView;
/**
* 沙漏视图类Java 7语法修复ProgressDrawable和setHeight问题
*/
public class HourglassView extends LinearLayout {
public static final String TAG = "HourglassView";
// 数据模型
private String hourglassId;
private int hour; // 小时
private int minute; // 分钟
private boolean isEnabled; // 开关状态
// 控件引用
private EditText etHour;
private EditText etMinute;
private ProgressBar progressBar;
private Switch switchControl;
// 样式参数
private int textSize = 16;
private int padding = 8;
private int progressColor = 0xFF2196F3; // 进度条颜色
private int progressBgColor = 0xFFE0E0E0; // 进度条背景色
private int textColor = 0xFF333333;
private int editTextWidth = 40; // 输入框宽度dp
private int progressHeight = 8; // 进度条高度dp新增参数
public HourglassView(Context context) {
super(context);
initView();
}
public HourglassView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
/**
* 初始化视图布局
*/
private void initView() {
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL);
setPadding(dp2px(padding), dp2px(padding), dp2px(padding), dp2px(padding));
// 1. 左侧时间输入区域(水平布局)
LinearLayout inputLayout = new LinearLayout(getContext());
inputLayout.setOrientation(HORIZONTAL);
inputLayout.setGravity(Gravity.CENTER_VERTICAL);
LayoutParams inputParams = new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
inputParams.setMargins(0, 0, dp2px(padding * 2), 0);
addView(inputLayout, inputParams);
// 小时输入框
etHour = createNumberEditText();
etHour.setHint("");
etHour.setFilters(new InputFilter[]{new InputFilter.LengthFilter(2)});
inputLayout.addView(etHour, getEditTextParams());
// 分隔符
TextView divider = new TextView(getContext());
divider.setText(":");
divider.setTextSize(textSize);
divider.setTextColor(textColor);
LayoutParams dividerParams = new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
dividerParams.setMargins(dp2px(padding / 2), 0, dp2px(padding / 2), 0);
inputLayout.addView(divider, dividerParams);
// 分钟输入框
etMinute = createNumberEditText();
etMinute.setHint("");
etMinute.setFilters(new InputFilter[]{new InputFilter.LengthFilter(2)});
inputLayout.addView(etMinute, getEditTextParams());
// 2. 中间进度条修复通过LayoutParams设置高度替代setHeight
progressBar = new ProgressBar(getContext(), null, android.R.attr.progressBarStyleHorizontal);
progressBar.setProgressDrawable(createProgressDrawable()); // 传入Drawable类型
// 修复核心用LayoutParams设置进度条高度兼容低版本
LayoutParams progressParams = new LayoutParams(
0,
dp2px(progressHeight), // 直接在布局参数中设置高度dp转px
1.0f
);
progressParams.setMargins(0, 0, dp2px(padding * 2), 0);
addView(progressBar, progressParams);
// 3. 右侧开关
switchControl = new Switch(getContext());
switchControl.setOnCheckedChangeListener(new Switch.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(android.widget.CompoundButton buttonView, boolean isChecked) {
isEnabled = isChecked;
// 开关状态控制输入框是否可编辑
etHour.setEnabled(!isChecked);
etMinute.setEnabled(!isChecked);
// 更新进度条(仅在开关开启时生效)
if (isChecked) {
updateProgressBar();
}
}
});
addView(switchControl);
// 初始状态
isEnabled = false;
etHour.setEnabled(true);
etMinute.setEnabled(true);
}
/**
* 创建数字输入框
*/
private EditText createNumberEditText() {
EditText editText = new EditText(getContext());
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setTextSize(textSize);
editText.setTextColor(textColor);
editText.setGravity(Gravity.CENTER);
editText.setSingleLine(true);
editText.setBackgroundResource(android.R.drawable.edit_text); // 默认输入框背景
return editText;
}
/**
* 获取输入框布局参数
*/
private LayoutParams getEditTextParams() {
LayoutParams params = new LayoutParams(
dp2px(editTextWidth),
ViewGroup.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, 0, dp2px(padding), 0);
return params;
}
/**
* 修复核心创建ProgressDrawable返回Drawable类型而非Paint
* 用LayerDrawable实现「背景+进度」的双层进度条
*/
private Drawable createProgressDrawable() {
// 1. 进度条背景(灰色)
ColorDrawable bgDrawable = new ColorDrawable(progressBgColor);
// 2. 进度条前景(主题色)
ColorDrawable progressDrawable = new ColorDrawable(progressColor);
// 3. 用ClipDrawable包裹前景实现进度裁剪
ClipDrawable clipDrawable = new ClipDrawable(progressDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
// 4. 组合成LayerDrawable顺序背景在下进度在上
Drawable[] layers = new Drawable[]{bgDrawable, clipDrawable};
LayerDrawable layerDrawable = new LayerDrawable(layers);
// 5. 设置进度条的层级ID必须与系统ProgressBar的ID匹配
layerDrawable.setId(0, android.R.id.background);
layerDrawable.setId(1, android.R.id.progress);
return layerDrawable;
}
/**
* 更新进度条(总时间 = 小时*60 + 分钟,单位:分钟)
*/
private void updateProgressBar() {
try {
// 获取输入的时间为空时默认0
int inputHour = TextUtils.isEmpty(etHour.getText().toString().trim())
? 0 : Integer.parseInt(etHour.getText().toString().trim());
int inputMinute = TextUtils.isEmpty(etMinute.getText().toString().trim())
? 0 : Integer.parseInt(etMinute.getText().toString().trim());
// 校验时间合法性小时0-99分钟0-59
inputHour = Math.max(0, Math.min(99, inputHour));
inputMinute = Math.max(0, Math.min(59, inputMinute));
// 计算总分钟数(进度条最大值)
int totalMinutes = inputHour * 60 + inputMinute;
totalMinutes = Math.max(1, totalMinutes); // 最小1分钟避免进度条无长度
// 更新进度条
progressBar.setMax(totalMinutes);
progressBar.setProgress(totalMinutes); // 初始显示满进度,可根据实际需求修改
// 更新数据模型
this.hour = inputHour;
this.minute = inputMinute;
} catch (NumberFormatException e) {
// 输入非法时重置进度条
progressBar.setMax(0);
progressBar.setProgress(0);
}
}
/**
* dp转px适配不同设备
*/
private int dp2px(int dp) {
return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
}
// ------------------- 数据模型 getter/setter -------------------
public String getHourglassId() {
return hourglassId;
}
public void setHourglassId(String hourglassId) {
this.hourglassId = hourglassId;
}
public int getHour() {
return hour;
}
public void setHour(int hour) {
this.hour = Math.max(0, Math.min(99, hour)); // 限制范围
etHour.setText(String.valueOf(this.hour));
}
public int getMinute() {
return minute;
}
public void setMinute(int minute) {
this.minute = Math.max(0, Math.min(59, minute)); // 限制范围
etMinute.setText(String.valueOf(this.minute));
}
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean enabled) {
isEnabled = enabled;
switchControl.setChecked(enabled);
}
/**
* 手动更新进度条(外部调用)
*/
public void refreshProgress() {
if (isEnabled) {
updateProgressBar();
}
}
// 工具类判断字符串是否为空Java7无TextUtils.isEmpty手动实现
private static class TextUtils {
public static boolean isEmpty(CharSequence str) {
return str == null || str.length() == 0;
}
}
}

View File

@@ -33,10 +33,6 @@ import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import cc.winboll.studio.positions.App;
import cc.winboll.studio.positions.AppLevel;
import cc.winboll.studio.positions.activities.WinBoLLActivity;
import cc.winboll.studio.positions.PointLevel;
public class PositionTaskListView extends LinearLayout {
// 视图模式常量
@@ -384,7 +380,7 @@ public class PositionTaskListView extends LinearLayout {
// 步骤3刷新Adapter局部刷新+范围通知,避免列表错乱)
notifyItemRemoved(position);
notifyItemRangeChanged(position, mAdapterData.size());
LogUtils.d(TAG, "Adapter已移除任务刷新列表位置索引=" + position + "");
// 步骤4通知外部如Activity任务已更新
@@ -461,7 +457,7 @@ public class PositionTaskListView extends LinearLayout {
}
});
}
private String genSelectedTimeText(long timeMillis) {
// 2. 格式化时间字符串Java 7 用 SimpleDateFormat需处理 ParseException
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
@@ -487,16 +483,6 @@ public class PositionTaskListView extends LinearLayout {
final EditText etEditDistance = dialogView.findViewById(R.id.et_edit_distance);
Button btnCancel = dialogView.findViewById(R.id.btn_dialog_cancel);
Button btnSave = dialogView.findViewById(R.id.btn_dialog_save);
HourglassView hourglassView = dialogView.findViewById(R.id.hourglassView);
if (WinBoLLActivity._mPointLevel == PointLevel.WUKONG) {
hourglassView.setVisibility(View.GONE);
} else if (WinBoLLActivity._mPointLevel == PointLevel.LAOJUN) {
hourglassView.setHourglassId("hourglass_001");
hourglassView.setHour(1);
hourglassView.setMinute(30);
hourglassView.setEnabled(false); // 开启开关
}
// 绑定外层对话框内的控件

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 禁用状态:仅此处自定义(灰化样式) -->
<item android:state_enabled="false">
<shape android:shape="rectangle">
<solid android:color="#E0E0E0" /> <!-- 禁用背景灰(浅灰,贴近系统禁用色) -->
<stroke android:width="1px" android:color="#CCCCCC" /> <!-- 禁用边框灰 -->
<corners android:radius="2dp" /> <!-- 匹配系统按钮圆角弧度 -->
</shape>
</item>
<!-- 启用状态:直接复用系统默认按钮样式(与普通按钮完全一致) -->
<item android:state_enabled="true" android:drawable="@android:drawable/btn_default" />
</selector>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 禁用状态:文字灰(系统默认禁用文字色) -->
<item android:state_enabled="false" android:color="#9E9E9E" />
<!-- 启用状态:复用系统默认按钮文字色(与普通按钮一致) -->
<item android:state_enabled="true" android:color="@android:color/black" />
</selector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> <!-- 矩形形状(匹配 LinearLayout -->
<!-- 1px 边框关键stroke 标签控制边框) -->
<stroke
android:width="2px"
android:color="?attr/colorAccent" /> <!-- 边框颜色(替换为你的颜色,如 #CCCCCC -->
<!-- 可选:设置 LinearLayout 背景色(若需要) -->
<solid android:color="#00000000" /> <!-- 内部填充色,默认透明可删除 -->
<!-- 可选:设置圆角(不需要圆角可删除) -->
<corners android:radius="0dp" />
</shape>

View File

@@ -1,67 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/layout_location_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="实时位置信息"
android:textSize="22sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_longitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前经度:等待更新..."
android:textSize="18sp"
android:layout_marginTop="15dp"/>
<TextView
android:id="@+id/tv_latitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前纬度:等待更新..."
android:textSize="18sp"
android:layout_marginTop="10dp"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_position_list"
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/layout_location_info"
android:layout_above="@id/fab_p_button"
android:layout_marginTop="20dp"
android:paddingBottom="10dp"/>
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"/>
<Button
android:id="@+id/fab_p_button"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="20dp"
android:background="@drawable/circle_button_bg"
android:text="P"
android:textColor="@android:color/white"
android:textSize="24sp"
android:elevation="6dp"
android:padding="0dp"
android:onClick="addNewPosition"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="20dp"
android:layout_weight="1.0">
</RelativeLayout>
<LinearLayout
android:id="@+id/layout_location_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="实时位置信息"
android:textSize="22sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tv_longitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前经度:等待更新..."
android:textSize="18sp"
android:layout_marginTop="15dp"/>
<TextView
android:id="@+id/tv_latitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前纬度:等待更新..."
android:textSize="18sp"
android:layout_marginTop="10dp"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_position_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/layout_location_info"
android:layout_above="@id/fab_p_button"
android:layout_marginTop="20dp"
android:paddingBottom="10dp"/>
<Button
android:id="@+id/fab_p_button"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="20dp"
android:background="@drawable/circle_button_bg"
android:text="P"
android:textColor="@android:color/white"
android:textSize="24sp"
android:elevation="6dp"
android:padding="0dp"
android:onClick="addNewPosition"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -1,39 +1,73 @@
<?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"
android:orientation="vertical">
android:id="@+id/llmain">
<androidx.appcompat.widget.Toolbar
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
android:gravity="center_vertical"/>
<Switch
android:id="@+id/switch_service_control"
android:layout_margin="16dp"
android:text="GPS服务开关"
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="0dp"
android:background="#00000000"
android:layout_weight="1.0">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:onClick="onPositions"
android:text="位置与任务管理"
android:id="@+id/btn_manage_positions"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/activity_background"
android:layout_margin="0dp">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:onClick="onLog"
android:text="查看应用日志"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1.0">
<Switch
android:id="@+id/switch_service_control"
android:text="GPS服务开关"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:background="@drawable/shape_2px_border"
android:paddingLeft="10dp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:onClick="onPositions"
android:text="位置与任务管理"
android:id="@+id/btn_manage_positions"
android:background="@drawable/btn_selector"
android:textColor="@drawable/btn_text_selector"
android:padding="12dp"/>
</LinearLayout>
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"
android:layout_alignParentBottom="true"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -10,23 +10,7 @@
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CheckPermission"
android:padding="10dp"
android:onClick="onCheckPermission"/>
</LinearLayout>
android:gravity="center_vertical"/>
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"

View File

@@ -65,16 +65,6 @@
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<cc.winboll.studio.positions.views.HourglassView
android:id="@+id/hourglassView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
@@ -87,7 +77,7 @@
android:layout_height="wrap_content"
android:text="开始时间"
android:id="@+id/btn_select_time"/>
<TextView
android:id="@+id/tv_selected_time"
android:layout_width="0dp"
@@ -95,6 +85,7 @@
android:text="Text"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-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_settings"
android:title="Settings"/>
</menu>

View File

@@ -1,9 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">悟空笔记</string>
<string name="appplus_name">时空任务</string>
<string name="open_appplus">开疆扩土</string>
<string name="close_appplus">返璞归真</string>
<string name="appplus_open_disabled">余力不足</string>
<string name="appplus_close_disabled">辎重难返</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="toolbar_height">60dp</dimen>
<dimen name="text_content_size">18dp</dimen>
<dimen name="text_title_size">24dp</dimen>
<dimen name="text_subtitle_size">16dp</dimen>
</resources>

View File

@@ -1,8 +1,9 @@
<resources>
<string name="app_name">Positions</string>
<string name="appplus_name">PositionsPlus</string>
<string name="appplus_name">PositionsPlus</string>
<string name="open_appplus">Open APP Plus</string>
<string name="close_appplus">Close APP Plus</string>
<string name="appplus_open_disabled">APP Plus Open Disable</string>
<string name="appplus_close_disabled">APP Plus Close Disable</string>
</resources>

View File

@@ -14,4 +14,12 @@
</style>
<!-- 设置Toolbar标题字体的大小 -->
<style name="Toolbar.TitleText" parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title">
<item name="android:textSize">@dimen/text_title_size</item>
</style>
<style name="Toolbar.SubTitleText" parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title">
<item name="android:textSize">@dimen/text_subtitle_size</item>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="BaseBean"
path="BaseBean/" />
</paths>

View File

@@ -29,11 +29,11 @@ android {
applicationId "cc.winboll.studio.powerbell"
minSdkVersion 23
targetSdkVersion 30
versionCode 7
versionCode 6
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.14"
versionName "15.11"
if(true) {
versionName = genVersionName("${versionName}")
}
@@ -56,12 +56,7 @@ 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
@@ -82,13 +77,8 @@ dependencies {
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
//api 'androidx.fragment:fragment:1.1.0'
// WinBoLL库 nexus.winboll.cc 地址
//api 'cc.winboll.studio:libaes:15.12.0'
//api 'cc.winboll.studio:libappbase:15.12.2'
// WinBoLL备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:AES:aes-v15.12.3'
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
implementation 'cc.winboll.studio:libaes:15.11.6'
implementation 'cc.winboll.studio:libappbase:15.11.0'
//api fileTree(dir: 'libs', include: ['*.aar'])
api fileTree(dir: 'libs', include: ['*.jar'])

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Dec 14 19:58:14 HKT 2025
stageCount=7
#Wed Nov 26 16:27:33 HKT 2025
stageCount=9
libraryProject=
baseVersion=15.14
publishVersion=15.14.6
baseVersion=15.11
publishVersion=15.11.8
buildCount=0
baseBetaVersion=15.14.7
baseBetaVersion=15.11.9

View File

@@ -4,56 +4,55 @@
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
<!-- ====================== 原有权限保留 + 补充核心权限 ====================== -->
<!-- 运行前台服务(原有保留,补充 Android 12+ 特殊前台服务权限) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 开机启动(原有保留,确保自启广播生效) -->
<!-- 只有在前台运行时才能获取大致位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 开机启动 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- 显示通知(原有保留,前台服务/保活必备) -->
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 显示通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- 应用使用统计相关(原有保留,忽略保护权限警告) -->
<!-- PACKAGE_USAGE_STATS -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<!-- BATTERY_STATS -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<!-- 相机相关(原有保留,补充权限等级声明,避免安装警告) -->
<uses-feature
android:name="android.hardware.camera"
android:required="false"/> <!-- 非核心功能设为非必须,兼容无相机设备 -->
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false"/>
<uses-permission android:name="android.permission.CAMERA" /> <!-- 补充相机权限原有仅声明feature无权限 -->
<!-- 应用信息相关(原有保留) -->
<!-- 计算应用存储空间 -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
<!-- 新增:文件管理权限(对应 PermissionUtils 全文件管理逻辑) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- API30+ 全文件权限 -->
<!-- 新增:忽略电池优化权限(对应 PermissionUtils 电池优化逻辑,必须声明) -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- 新增API30+ 跳转系统权限页兼容(避免自启/文件权限跳转失败) -->
<queries>
<!-- 全文件管理权限跳转兼容 -->
<!-- 小米自启权限跳转兼容(精准匹配安全中心包名) -->
<package android:name="com.miui.securitycenter" />
<!-- 电池优化权限跳转兼容 -->
</queries>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<application
android:name=".App"
@@ -65,20 +64,17 @@
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
tools:ignore="GoogleAppIndexingWarning">
<!-- ====================== 页面配置(原有保留,优化 exported 安全) ====================== -->
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:launchMode="singleTask">
</activity>
<activity
android:name=".activities.CrashActivity"
android:exported="false"/> <!-- 新增:非外部调用,设为 false提升安全 -->
<activity android:name=".activities.CrashActivity"/>
<activity-alias
android:name=".MainActivityEN1"
@@ -87,13 +83,19 @@
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmainen1"/>
</activity-alias>
<activity-alias
@@ -103,13 +105,19 @@
android:label="@string/app_name_cn1"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn1"/>
</activity-alias>
<activity-alias
@@ -119,121 +127,109 @@
android:label="@string/app_name_cn2"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn2"/>
</activity-alias>
<activity
android:name=".activities.ClearRecordActivity"
android:name="cc.winboll.studio.powerbell.activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask"
android:exported="false"/> <!-- 新增:非外部调用,设为 false -->
android:launchMode="singleTask">
</activity>
<activity
android:name=".activities.BackgroundSettingsActivity"
android:name="cc.winboll.studio.powerbell.activities.BackgroundPictureActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/>
<data android:mimeType="image/webp"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
<!-- ====================== 广播接收器(优化自启广播,提升保活成功率) ====================== -->
<receiver
android:name=".receivers.MainReceiver"
android:enabled="true"
android:exported="true"
android:exported="false"
android:directBootAware="true">
<intent-filter android:priority="1000"> <!-- 新增:广播优先级最高,确保优先接收开机广播 -->
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<!-- 补充:充电/解锁广播,增强后台唤醒机会 -->
<action android:name="android.intent.action.POWER_CONNECTED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter>
</receiver>
<!-- ====================== 服务配置(核心优化:前台服务保活,适配 API29-30 ====================== -->
<!-- 核心前台服务ControlCenterService保活核心重点优化 -->
<service
android:name=".services.ControlCenterService"
android:name="cc.winboll.studio.powerbell.services.ControlCenterService"
android:priority="1000"
android:enabled="true"
android:exported="false"
android:process=".controlcenterservice"
android:foregroundServiceType="dataSync">
<!-- 新增Android 12+ 前台服务用途声明(系统强制,否则拦截服务启动) -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="后台核心功能运行、持续保活" /> <!-- 按实际用途填写,不可空 -->
</service>
android:process=".controlcenterservice"/>
<!-- 辅助服务AssistantService按需优化增强稳定性 -->
<service
android:name=".services.AssistantService"
android:name="cc.winboll.studio.powerbell.services.AssistantService"
android:enabled="true"
android:exported="false"
android:process=".assistantservice"> <!-- 若需前台启动,添加此配置;纯后台可移除 -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行" />
</service>
android:process=".assistantservice"/>
<!-- ====================== 其他配置(原有保留,补充优化) ====================== -->
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<!-- 所有非外部调用的 Activity统一设 exported=false提升安全 -->
<activity
android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity
android:name=".activities.PixelPickerActivity"
android:exported="false"/>
<activity
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:name=".activities.SettingsActivity"
android:exported="false"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReporterActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.AboutActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/>
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTestActivity"/>
<!-- 文件提供者(原有保留,正常使用) -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
<!-- UCrop 第三方页面原有保留exported=true 正常) -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true">
</activity>
<activity android:name="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"/>
</application>
</manifest>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

@@ -0,0 +1,65 @@
package cc.winboll.studio.powerbell.activities;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/25 01:16:32
* @Describe 应用介绍窗口
*/
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import cc.winboll.studio.libaes.models.APPInfo;
import cc.winboll.studio.libaes.views.AToolbar;
import cc.winboll.studio.libaes.views.AboutView;
import cc.winboll.studio.powerbell.R;
public class AboutActivity extends Activity {
Context mContext;
public static final String TAG = "AboutActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
mContext = this;
// 初始化工具栏
AToolbar mAToolbar = (AToolbar) findViewById(R.id.toolbar);
setActionBar(mAToolbar);
mAToolbar.setSubtitle(getString(R.string.text_about));
//mAToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getActionBar().setDisplayHomeAsUpEnabled(true);
AboutView aboutView = CreateAboutView();
// 在 Activity 的 onCreate 或其他生命周期方法中调用
LinearLayout llRoot = findViewById(R.id.root_ll);
//layout.setOrientation(LinearLayout.VERTICAL);
// 创建布局参数(宽度和高度)
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
llRoot.addView(aboutView, params);
}
public AboutView CreateAboutView() {
String szBranchName = "powerbell";
APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_launcher);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("APPBase");
appInfo.setAppGitOwner("Studio");
appInfo.setAppGitAPPBranch(szBranchName);
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell");
appInfo.setAppAPKName("PowerBell");
appInfo.setAppAPKFolderName("PowerBell");
return new AboutView(mContext, appInfo);
}
}

View File

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

View File

@@ -1,868 +0,0 @@
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.text.TextUtils;
import android.view.View;
import android.widget.LinearLayout;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.FileProvider;
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.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.UriUtils;
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 {
// ====================== 常量定义 ======================
public static final String TAG = "BackgroundSettingsActivity";
private static final int SDK_VERSION_TIRAMISU = 33;
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 REQUEST_PIXELPICKER = 1001;
// ====================== 成员变量 ======================
private BackgroundSourceUtils mBgSourceUtils;
private BitmapCacheUtils mBitmapCache;
private Toolbar mToolbar;
private BackgroundView mBackgroundView;
private File mfTakePhoto;
volatile boolean isCommitSettings = false;
volatile boolean isPreviewBackgroundChanged = 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);
LogUtils.d(TAG, "【生命周期】onCreate 开始初始化");
// 初始化视图与工具类
mBackgroundView = findViewById(R.id.background_view);
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
mBitmapCache = BitmapCacheUtils.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();
// 处理分享意图或初始化预览
if (handleShareIntent()) {
ToastUtils.show("handleShareIntent");
} else {
mBgSourceUtils.setCurrentSourceToPreview();
}
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(mBgSourceUtils.getPreviewBackgroundBean());
doubleRefreshPreview();
LogUtils.d(TAG, "【生命周期】onCreate 初始化完成");
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
doubleRefreshPreview();
LogUtils.d(TAG, "【生命周期】onPostCreate 执行双重刷新");
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "【回调触发】requestCode" + requestCode + "resultCode" + resultCode);
try {
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;
case REQUEST_PIXELPICKER:
handlePixelPickerResult(requestCode, resultCode, data);
break;
default:
LogUtils.d(TAG, "【回调忽略】未知requestCode");
break;
}
} catch (Exception e) {
LogUtils.e(TAG, "【回调异常】" + e.getMessage());
ToastUtils.show("操作失败");
}
}
@Override
public void finish() {
LogUtils.d(TAG, "【生命周期】finish 触发isCommitSettings" + isCommitSettings + "isPreviewBackgroundChanged" + isPreviewBackgroundChanged);
if (isCommitSettings) {
super.finish();
} else {
if (isPreviewBackgroundChanged) {
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
@Override
public void onYes() {
//ToastUtils.show("onYes");
mBgSourceUtils.commitPreviewSourceToCurrent();
isCommitSettings = true;
finish();
}
@Override
public void onNo() {
isCommitSettings = true;
finish();
}
});
} else {
isCommitSettings = true;
finish();
}
}
}
// ====================== 界面初始化方法 ======================
private void initToolbar() {
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
}
private void initClickListeners() {
LogUtils.d(TAG, "【界面初始化】绑定按钮点击事件");
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 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, "【按钮点击】选择图片");
launchImageSelector();
}
};
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, "【按钮点击】自由裁剪");
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, "【按钮点击】拍照");
// 移除:旧文件删除逻辑
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;
}
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, "【拍照失败】" + e.getMessage());
}
}
};
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.getPreviewBackgroundBean().getBackgroundFilePath();
File targetFile = new File(targetImagePath);
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
ToastUtils.show("无有效图片可拾取像素");
LogUtils.e(TAG, "【像素拾取失败】文件无效:" + targetImagePath);
return;
}
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
intent.putExtra("imagePath", targetImagePath);
startActivityForResult(intent, REQUEST_PIXELPICKER);
LogUtils.d(TAG, "【像素拾取启动】路径:" + targetImagePath);
}
};
private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】清空像素颜色");
BackgroundBean bean = mBgSourceUtils.getPreviewBackgroundBean();
int oldColor = bean.getPixelColor();
bean.setPixelColor(0);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
ToastUtils.show("像素颜色已清空");
LogUtils.d(TAG, "【像素清空】旧颜色:" + oldColor);
}
};
// ====================== 工具方法 ======================
/**
* 生成 FileProvider Uri适配 Android 7.0+
*/
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;
}
}
/**
* 校验 Bitmap 是否有效(未被回收且不为空)
*/
private boolean isBitmapValid(Bitmap bitmap) {
return bitmap != null && !bitmap.isRecycled();
}
/**
* 双重刷新预览,确保背景加载最新数据
* 移除:缓存清空逻辑
*/
private void doubleRefreshPreview() {
LogUtils.d(TAG, "【工具方法】doubleRefreshPreview 开始执行");
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
LogUtils.w(TAG, "【双重刷新】跳过对象为空或Activity已结束");
return;
}
// 第一重刷新
try {
mBgSourceUtils.loadSettings();
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean(), true);
mBackgroundView.setBackgroundColor(mBgSourceUtils.getPreviewBackgroundBean().getPixelColor());
LogUtils.d(TAG, "【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
return;
}
// 第二重刷新(延迟执行)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing() && mBgSourceUtils != null) {
try {
mBgSourceUtils.loadSettings();
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean(), true);
mBackgroundView.setBackgroundColor(mBgSourceUtils.getPreviewBackgroundBean().getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
}
}
}
}, 200);
}
/**
* 解析裁剪临时文件为 Bitmap带采样率优化
*/
private Bitmap parseCropTempFileToBitmap(File cropTempFile) {
LogUtils.d(TAG, "【工具方法】parseCropTempFileToBitmap 解析文件:" + (cropTempFile != null ? cropTempFile.getAbsolutePath() : "null"));
if (cropTempFile == null || !cropTempFile.exists() || !cropTempFile.isFile() || cropTempFile.length() <= 100) {
LogUtils.e(TAG, "【Bitmap解析】文件无效");
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, "【Bitmap解析】采样率" + sampleSize);
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inInputShareable = true;
try {
Bitmap cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options);
if (!isBitmapValid(cropBitmap)) {
LogUtils.e(TAG, "【Bitmap解析】解析失败");
return null;
}
LogUtils.d(TAG, "【Bitmap解析】成功尺寸" + cropBitmap.getWidth() + "x" + cropBitmap.getHeight());
return cropBitmap;
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "【Bitmap解析】OOM异常");
return null;
} catch (Exception e) {
LogUtils.e(TAG, "【Bitmap解析】异常" + e.getMessage());
return null;
}
}
/**
* 调整 Bitmap 比例至目标比例
*/
private Bitmap adjustBitmapToFinalRatio(Bitmap originalBitmap, float finalCropRatio) {
LogUtils.d(TAG, "【工具方法】adjustBitmapToFinalRatio 调整比例,目标比例:" + finalCropRatio);
if (!isBitmapValid(originalBitmap) || finalCropRatio <= 0) {
LogUtils.e(TAG, "【比例调整】参数无效");
return null;
}
int originalWidth = originalBitmap.getWidth();
int originalHeight = originalBitmap.getHeight();
float originalRatio = (float) originalWidth / originalHeight;
if (Math.abs(originalRatio - finalCropRatio) < 0.001f) {
LogUtils.d(TAG, "【比例调整】比例一致,生成副本");
return originalBitmap.copy(originalBitmap.getConfig(), false);
}
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, "【比例调整】调整前:" + originalWidth + "x" + originalHeight + ",调整后:" + targetWidth + "x" + targetHeight);
try {
Bitmap adjustedBitmap = Bitmap.createBitmap(
originalBitmap,
(originalWidth - targetWidth) / 2,
(originalHeight - targetHeight) / 2,
targetWidth,
targetHeight
);
return adjustedBitmap;
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "【比例调整】OOM异常");
return null;
}
}
/**
* 保存 Bitmap 到目标文件
* 移除:原文件删除逻辑
*/
private void saveScaledBitmapToFile(Bitmap bitmap, File targetFile) {
LogUtils.d(TAG, "【工具方法】saveScaledBitmapToFile 保存图片:" + targetFile.getAbsolutePath());
if (!isBitmapValid(bitmap) || targetFile == null) {
LogUtils.e(TAG, "【图片保存】参数无效");
return;
}
OutputStream outputStream = null;
try {
outputStream = new BufferedOutputStream(new FileOutputStream(targetFile));
bitmap.compress(CompressFormat.JPEG, 100, outputStream);
outputStream.flush();
LogUtils.d(TAG, "【图片保存】成功,文件大小:" + targetFile.length() + " bytes");
} catch (IOException e) {
LogUtils.e(TAG, "【图片保存】异常:" + e.getMessage());
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【图片保存】关闭流异常");
}
}
}
}
/**
* 从裁剪文件中读取比例
*/
private float getRatioFromSystemCropFile(File systemCropFile) {
LogUtils.d(TAG, "【工具方法】getRatioFromSystemCropFile 读取比例:" + systemCropFile.getAbsolutePath());
if (systemCropFile == null || !systemCropFile.exists() || !systemCropFile.isFile()) {
LogUtils.e(TAG, "【比例读取】文件无效");
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, "【比例读取】尺寸无效");
return -1;
}
float systemRatio = (float) cropWidth / cropHeight;
LogUtils.d(TAG, "【比例读取】成功,比例:" + systemRatio);
return systemRatio;
}
// ====================== 业务逻辑方法 ======================
/**
* 处理分享意图
*/
private boolean 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, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener(){
@Override
public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
ToastUtils.show(String.format("uriRecivedPicture %s", uriRecivedPicture));
}
});
dlg.show();
LogUtils.d(TAG, "【分享处理】收到分享图片意图");
return true;
}
}
return false;
}
/**
* 判断是否为图片类型
*/
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");
}
/**
* 启动图片选择器
*/
private void launchImageSelector() {
LogUtils.d(TAG, "【业务逻辑】launchImageSelector 启动选择器");
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 (Intent intent : intents) {
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();
}
});
}
}
/**
* 处理存储权限回调
*/
private void handleStoragePermissionCallback() {
if (Environment.isExternalStorageManager()) {
LogUtils.d(TAG, "【权限回调】已授予");
ToastUtils.show("存储权限已获取");
} else {
LogUtils.d(TAG, "【权限回调】已拒绝");
ToastUtils.show("存储权限不足");
}
}
/**
* 处理操作取消或失败
*/
private void handleOperationCancelOrFail() {
mBgSourceUtils.setCurrentSourceToPreview();
LogUtils.d(TAG, "【业务逻辑】操作取消或失败,恢复预览");
ToastUtils.show("操作取消或失败");
doubleRefreshPreview();
}
/**
* 处理拍照结果
*/
private void handleTakePhotoResult(int resultCode, Intent data) {
LogUtils.d(TAG, "【业务逻辑】handleTakePhotoResult 处理拍照结果");
if (resultCode != RESULT_OK || data == null) {
handleOperationCancelOrFail();
return;
}
if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) {
ToastUtils.show("拍照文件无效");
return;
}
Bitmap photoBitmap = getTakePhotoBitmap(data);
if (isBitmapValid(photoBitmap)) {
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, "【拍照完成】已启动裁剪");
}
/**
* 解析拍照返回的 Bitmap
*/
private Bitmap getTakePhotoBitmap(Intent data) {
LogUtils.d(TAG, "【业务逻辑】getTakePhotoBitmap 解析拍照Bitmap");
if (mfTakePhoto != null && mfTakePhoto.exists()) {
LogUtils.d(TAG, "【拍照Bitmap解析】从文件解析");
Bitmap photoBitmap = parseCropTempFileToBitmap(mfTakePhoto);
if (isBitmapValid(photoBitmap)) {
LogUtils.d(TAG, "【拍照Bitmap解析】成功");
return photoBitmap;
} else {
LogUtils.w(TAG, "【拍照Bitmap解析】文件解析失败尝试Intent");
}
} else {
LogUtils.w(TAG, "【拍照Bitmap解析】文件无效尝试Intent");
}
if (data != null) {
try {
Bitmap thumbnailBitmap = data.getParcelableExtra("data");
if (isBitmapValid(thumbnailBitmap)) {
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) {
LogUtils.d(TAG, "【业务逻辑】handleSelectPictureResult 处理选图结果");
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 >= SDK_VERSION_TIRAMISU) {
getContentResolver().takePersistableUriPermission(
selectedImage,
Intent.FLAG_GRANT_READ_URI_PERMISSION
);
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
}
if (putUriFileToPreviewSource(selectedImage)) {
LogUtils.d(TAG, "【选图同步】路径绑定完成");
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
mBgSourceUtils.getPreviewBackgroundBean(),
mBackgroundView.getWidth(),
mBackgroundView.getHeight(),
false,
REQUEST_CROP_IMAGE
);
} else {
ToastUtils.show("图片同步失败");
LogUtils.e(TAG, "【选图同步】文件复制失败");
}
}
/**
* 将 Uri 文件同步到预览 Bean
*/
boolean putUriFileToPreviewSource(Uri srcUriFile) {
String filePath = UriUtils.getFilePathFromUri(this, srcUriFile);
if (TextUtils.isEmpty(filePath)) {
LogUtils.e(TAG, "putUriFileToPreviewSource: Uri解析路径为空");
return false;
}
File srcFile = new File(filePath);
return putUriFileToPreviewSource(srcFile);
}
/**
* 将 File 同步到预览 Bean
* 保留核心修复逻辑更新预览Bean路径
*/
boolean putUriFileToPreviewSource(File srcFile) {
LogUtils.d(TAG, String.format("putUriFileToPreviewSource(File srcFile) srcFile %s", srcFile));
mBgSourceUtils.loadSettings();
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
File dstFile = new File(previewBean.getBackgroundFilePath());
LogUtils.d(TAG, String.format("putUriFileToPreviewSource(File srcFile) dstFile %s", dstFile));
if (FileUtils.copyFile(srcFile, dstFile)) {
LogUtils.d(TAG, "putUriFileToPreviewSource(File srcFile) 文件拷贝成功。 ");
return true;
}
LogUtils.d(TAG, "putUriFileToPreviewSource(File srcFile) 文件无法拷贝。 ");
return false;
}
/**
* 处理裁剪结果
*/
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) {
isPreviewBackgroundChanged = true;
LogUtils.d(TAG, "【裁剪结果】裁剪成功");
final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(true);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
// float systemFileRatio = getRatioFromSystemCropFile(cropTempFile);
// if (systemFileRatio > 0) {
// Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
// if (isBitmapValid(cropBitmap)) {
// Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio);
// if (isBitmapValid(scaledCropBitmap)) {
// saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
// scaledCropBitmap.recycle();
// }
// cropBitmap.recycle();
// } else {
// LogUtils.e(TAG, "【裁剪结果】裁剪Bitmap解析无效");
// }
// }
//
// new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
// @Override
// public void run() {
// if (!isFinishing()) {
// doubleRefreshPreview();
// LogUtils.d(TAG, "【裁剪结果】触发双重刷新");
// }
// }
// }, 300);
} else {
handleOperationCancelOrFail();
}
}
private void handlePixelPickerResult(int requestCode, int resultCode, Intent data) {
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
}
}

View File

@@ -23,11 +23,8 @@ import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import java.util.ArrayList;
import java.util.Collections;
@@ -35,11 +32,11 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cc.winboll.studio.libappbase.LogUtils;
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
public class BatteryReportActivity extends Activity {
public static final String TAG = "BatteryReportActivity";
private Toolbar mToolbar;
private RecyclerView rvBatteryReport;
private BatteryReportAdapter adapter;
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
@@ -53,34 +50,10 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
private Map<String, String> packageToAppNameCache = new HashMap<String, String>();
private PackageManager mPackageManager;
@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_battery_report);
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
mPackageManager = getPackageManager();
// 权限检查Java7 传统条件判断)

View File

@@ -7,37 +7,26 @@ import android.view.View;
import android.widget.Switch;
import android.widget.TextView;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.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.models.BatteryInfoBean;
import cc.winboll.studio.powerbell.beans.BatteryInfoBean;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.StringUtils;
import java.util.ArrayList;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity {
public class ClearRecordActivity extends Activity {
public static final String TAG = "ClearRecordActivity";
private Toolbar mToolbar;
AToolbar mAToolbar;
TextView mtvRecordText;
App mApplication;
boolean mIsShowRecordWithEnter = false;
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -45,18 +34,21 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
mApplication = (App) getApplication();
// 初始化工具栏
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
mAToolbar = (AToolbar) findViewById(R.id.toolbar);
setActionBar(mAToolbar);
//mAToolbar.setTitle(getTitle() + " - " + getString(R.string.subtitle_activity_clearrecord));
mAToolbar.setSubtitle(R.string.subtitle_activity_clearrecord);
//mAToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
//mAToolbar.setSubtitleTextAppearance(this, R.style.Toolbar_SubTitleText);
//mAToolbar.setBackgroundColor(getColor(R.color.colorPrimary));
setActionBar(mAToolbar);
getActionBar().setDisplayHomeAsUpEnabled(true);
mAToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
// 设置滑动清理控件
//

View File

@@ -24,10 +24,10 @@ import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.AToolbar;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
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();
// 可以在这里添加确定后的回调逻辑
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getPreviewBackgroundBean();
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
bean.setPixelColor(pixelColor);
utils.saveSettings();
utils.saveData();
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
setBackgroundColor();
}
@@ -217,8 +217,8 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
void setBackgroundColor() {
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getPreviewBackgroundBean();
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this);
BackgroundPictureBean bean = utils.getBackgroundPictureBean();
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, BackgroundSettingsActivity.class);
intent.setClass(this, BackgroundPictureActivity.class);
startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), );
return true;
@@ -247,11 +247,9 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
@Override
public void onBackPressed() {
super.onBackPressed();
setResult(RESULT_OK);
finish();
// Intent intent = new Intent();
// intent.setClass(this, BackgroundSettingsActivity.class);
// startActivity(intent);
Intent intent = new Intent();
intent.setClass(this, BackgroundPictureActivity.class);
startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
}
}

View File

@@ -21,9 +21,6 @@ import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.R;
@@ -31,24 +28,15 @@ import cc.winboll.studio.powerbell.R;
public abstract class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "WinBoLLActivity";
protected volatile AESThemeBean.ThemeType mThemeType;
protected TextView mTagView;
@Override
protected void onCreate(Bundle savedInstanceState) {
mThemeType = getThemeType();
setThemeStyle();
super.onCreate(savedInstanceState);
}
AESThemeBean.ThemeType getThemeType() {
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
changeFullScreen(this);
}
void setThemeStyle() {
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
}
@Override
protected void onStart() {
@@ -91,13 +79,13 @@ public abstract class WinBoLLActivity extends AppCompatActivity implements IWinB
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
//GlobalApplication.getWinBoLLActivityManager().add(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
WinBoLLActivityManager.getInstance().registeRemove(this);
//GlobalApplication.getWinBoLLActivityManager().registeRemove(this);
}
@Override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,19 +2,22 @@ package cc.winboll.studio.powerbell.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
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.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.UriUtil;
import java.io.File;
import java.io.IOException;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -26,25 +29,21 @@ public class BackgroundPicturePreviewDialog extends Dialog {
public static final String TAG = "BackgroundPicturePreviewDialog";
Context mContext;
//BackgroundSourceUtils mBackgroundPictureUtils;
BackgroundPictureUtils mBackgroundPictureUtils;
Button dialogbackgroundpicturepreviewButton1;
Button dialogbackgroundpicturepreviewButton2;
//String mszPreReceivedFileName;
IOnRecivedPictureListener mIOnRecivedPictureListener;
Uri mUriRecivedPicture;
BackgroundView mBackgroundView;
String mszPreReceivedFileName;
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
public BackgroundPicturePreviewDialog(Context context) {
super(context);
setContentView(R.layout.dialog_backgroundpicturepreview);
mIOnRecivedPictureListener = iOnRecivedPictureListener;
//initEnv();
initEnv();
mContext = context;
//mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
mBackgroundPictureUtils = ((BackgroundPictureActivity)context).mBackgroundPictureUtils;
mBackgroundView = findViewById(R.id.backgroundview);
previewRecivedPicture();
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
copyAndViewRecivePicture(imageView);
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
@@ -54,7 +53,6 @@ public class BackgroundPicturePreviewDialog extends Dialog {
// 跳转到主窗口
Intent i = new Intent(mContext, MainActivity.class);
mContext.startActivity(i);
dismiss();
}
});
@@ -64,77 +62,79 @@ public class BackgroundPicturePreviewDialog extends Dialog {
@Override
public void onClick(View v) {
// 使用分享到的图片
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
//
//LogUtils.d(TAG, "mszReceivedFileName : " + mszReceivedFileName);
((IOnRecivedPictureListener)mContext).onAcceptRecivedPicture(mszPreReceivedFileName);
// 关闭对话框
dismiss();
}
});
}
// void initEnv() {
// LogUtils.d(TAG, "initEnv()");
// mszPreReceivedFileName = "PreReceived.data";
// }
void initEnv() {
LogUtils.d(TAG, "initEnv()");
mszPreReceivedFileName = "PreReceived.data";
}
void previewRecivedPicture() {
BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext);
void copyAndViewRecivePicture(ImageView imageView) {
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext());
BackgroundPictureActivity activity = ((BackgroundPictureActivity)mContext);
//取出文件uri
mUriRecivedPicture = activity.getIntent().getData();
if (mUriRecivedPicture == null) {
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
Uri uri = activity.getIntent().getData();
if (uri == null) {
uri = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
}
//获取文件真实地址
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
String szSrcImage = UriUtil.getFilePathFromUri(mContext, uri);
if (TextUtils.isEmpty(szSrcImage)) {
Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show();
dismiss();
return;
}
mBackgroundView.loadImage(szSrcImage);
//
// File fSrcImage = new File(szSrcImage);
// //mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
// File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
// // 复制源图片到剪裁文件
// try {
// FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
// LogUtils.d(TAG, "copyFileUsingFileChannels");
// Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
// imageView.setBackground(drawable);
// //LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
// } catch (IOException e) {
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// }
File fSrcImage = new File(szSrcImage);
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
File mfPreReceivedPhoto = new File(activity.mBackgroundPictureUtils.getBackgroundDir(), mszPreReceivedFileName);
// 复制源图片到剪裁文件
try {
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
LogUtils.d(TAG, "copyFileUsingFileChannels");
Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
imageView.setBackground(drawable);
//LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
//
// 创建图片背景图片目录
//
// boolean createBackgroundFolder2(String szBackgroundFolder) {
// // 文件路径参数为空值或无效值时返回false.
// if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
// return false;
// }
//
// LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
// File f = new File(szBackgroundFolder);
// if (f.exists()) {
// if (f.isDirectory()) {
// return true;
// } else {
// // 工作路径不是一个目录
// LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
// return false;
// }
// } else {
// return f.mkdirs();
// }
// }
boolean createBackgroundFolder2(String szBackgroundFolder) {
// 文件路径参数为空值或无效值时返回false.
if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
return false;
}
LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
File f = new File(szBackgroundFolder);
if (f.exists()) {
if (f.isDirectory()) {
return true;
} else {
// 工作路径不是一个目录
LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
return false;
}
} else {
return f.mkdirs();
}
}
public interface IOnRecivedPictureListener {
void onAcceptRecivedPicture(Uri uriRecivedPicture);
void onAcceptRecivedPicture(String szBackgroundFileName);
}
}

View File

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

View File

@@ -19,11 +19,8 @@ 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.models.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;
@@ -33,8 +30,328 @@ public class MainViewFragment extends Fragment {
public static final String TAG = "MainViewFragment";
public static final int MSG_RELOAD_APPCONFIG = 0;
public static final int MSG_CURRENTVALUEBATTERY = 1;
static MainViewFragment _mMainViewFragment;
AppConfigUtils mAppConfigUtils;
View mView;
Drawable mDrawableFrame;
LinearLayout mllLeftSeekBar;
LinearLayout mllRightSeekBar;
CheckBox mcbIsEnableChargeReminder;
CheckBox mcbIsEnableUsegeReminder;
Switch mswIsEnableService;
TextView mtvTips;
// 背景布局
//LinearLayout mLinearLayoutloadBackground;
// 现在电量图示
BatteryDrawable mCurrentValueBatteryDrawable;
// 现在充电提醒电量图示
BatteryDrawable mChargeReminderValueBatteryDrawable;
// 现在耗电提醒电量图示
BatteryDrawable mUsegeReminderValueBatteryDrawable;
ImageView mCurrentValueBatteryImageView;
ImageView mChargeReminderValueBatteryImageView;
ImageView mUsegeReminderValueBatteryImageView;
VerticalSeekBar mChargeReminderSeekBar;
ChargeReminderSeekBarChangeListener mChargeReminderSeekBarChangeListener;
TextView mtvChargeReminderValue;
VerticalSeekBar mUsegeReminderSeekBar;
UsegeReminderSeekBarChangeListener mUsegeReminderSeekBarChangeListener;
TextView mtvUsegeReminderValue;
CheckBox mcbUsegeReminderValue;
TextView mtvCurrentValue;
BackgroundView bvPreviewBackground;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.fragment_mainview, container, false);
_mMainViewFragment = MainViewFragment.this;
mAppConfigUtils = App.getAppConfigUtils(getActivity());
// 获取指定ID的View实例
bvPreviewBackground = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
/*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1);
// 注册OnGlobalLayoutListener
mainImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 获取宽度和高度
int width = mainImageView.getMeasuredWidth();
int height = mainImageView.getMeasuredHeight();
BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(getActivity());
BackgroundPictureBean bean = utils.loadBackgroundPictureBean();
bean.setBackgroundWidth(width);
bean.setBackgroundHeight(height);
utils.saveData();
// 移除监听器以避免内存泄漏
mainImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});*/
mDrawableFrame = getActivity().getDrawable(R.drawable.bg_frame);
mllLeftSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout1);
mllRightSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout2);
// 初始化充电电量提醒设置控件
mtvChargeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView2);
mChargeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar1);
mcbIsEnableChargeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox1);
// 初始化耗电电量提醒设置控件
mtvUsegeReminderValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView3);
mUsegeReminderSeekBar = (VerticalSeekBar) mView.findViewById(R.id.fragmentandroidviewVerticalSeekBar2);
mcbIsEnableUsegeReminder = mView.findViewById(R.id.fragmentmainviewCheckBox2);
// 初始化现在电量显示控件
mtvCurrentValue = (TextView) mView.findViewById(R.id.fragmentandroidviewTextView4);
// 初始化服务总开关
mswIsEnableService = (Switch) mView.findViewById(R.id.fragmentandroidviewSwitch1);
mtvTips = mView.findViewById(R.id.fragmentandroidviewTextView1);
// 设置视图显示数据
setViewData();
// 设置视图控件响应
setViewListener();
// 注册一个广播接收
//mMainActivityReceiver = new MainActivityReceiver(this);
//mMainActivityReceiver.registerAction();
// 启动的时候检查一下服务
if (mAppConfigUtils.getIsEnableService()
&& ServiceUtils.isServiceAlive(getActivity(), ControlCenterService.class.getName()) == false) {
// 如果配置了服务启动,服务没有启动
// 就启动服务
Intent intent = new Intent(getActivity(), ControlCenterService.class);
getActivity().startForegroundService(intent);
}
return mView;
}
void setViewData() {
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
int nUsegeReminderValue = mAppConfigUtils.getUsegeReminderValue();
int nCurrentValue = mAppConfigUtils.getCurrentValue();
mllLeftSeekBar.setBackground(mDrawableFrame);
mllRightSeekBar.setBackground(mDrawableFrame);
// 初始化电量图
mCurrentValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCurrent));
mCurrentValueBatteryDrawable.setValue(mAppConfigUtils.getCurrentValue());
mCurrentValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView1);
mCurrentValueBatteryImageView.setImageDrawable(mCurrentValueBatteryDrawable);
// 初始化充电电量提醒图
mChargeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorCharge));
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
mChargeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView3);
mChargeReminderValueBatteryImageView.setImageDrawable(mChargeReminderValueBatteryDrawable);
// 初始化耗电电量提醒图
mUsegeReminderValueBatteryDrawable = new BatteryDrawable(getActivity().getColor(R.color.colorUsege));
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
mUsegeReminderValueBatteryImageView = mView.findViewById(R.id.fragmentandroidviewImageView2);
mUsegeReminderValueBatteryImageView.setImageDrawable(mUsegeReminderValueBatteryDrawable);
// 初始化充电电量提醒设置控件
mtvChargeReminderValue.setTextColor(getActivity().getColor(R.color.colorCharge));
//LogUtils.d(TAG, "Color.YELLOW is " + Integer.toString(mApplication.getColor(R.color.colorCharge)));
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
mChargeReminderSeekBar.setProgress(nChargeReminderValue);
mcbIsEnableChargeReminder.setChecked(mAppConfigUtils.getIsEnableChargeReminder());
// 初始化耗电电量提醒设置控件
mtvUsegeReminderValue.setTextColor(getActivity().getColor(R.color.colorUsege));
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
mUsegeReminderSeekBar.setProgress(nUsegeReminderValue);
mcbIsEnableUsegeReminder.setChecked(mAppConfigUtils.getIsEnableUsegeReminder());
// 初始化现在电量显示控件
mtvCurrentValue.setTextColor(getActivity().getColor(R.color.colorCurrent));
mtvCurrentValue.setText(Integer.toString(nCurrentValue) + "%");
// 初始化服务总开关
mswIsEnableService.setChecked(mAppConfigUtils.getIsEnableService());
if (mAppConfigUtils.getIsEnableService()) {
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
ControlCenterService.startControlCenterService(getActivity());
} else {
//LogUtils.d(TAG, "mApplication.getIsEnableService() " + Boolean.toString(mAppConfigUtils.getIsEnableService()));
ControlCenterService.stopControlCenterService(getActivity());
}
mswIsEnableService.setText(getString(R.string.txt_aboveswitch));
mtvTips.setText(getString(R.string.txt_aboveswitchtips));
}
void setViewListener() {
// 初始化充电电量提醒设置控件
mChargeReminderSeekBarChangeListener = new ChargeReminderSeekBarChangeListener();
mChargeReminderSeekBar.setOnSeekBarChangeListener(mChargeReminderSeekBarChangeListener);
mcbIsEnableChargeReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "setIsEnableChargeReminder");
mAppConfigUtils.setIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
//ControlCenterService.updateIsEnableChargeReminder(mcbIsEnableChargeReminder.isChecked());
}
});
// 初始化耗电电量提醒设置控件
mUsegeReminderSeekBarChangeListener = new UsegeReminderSeekBarChangeListener();
mUsegeReminderSeekBar.setOnSeekBarChangeListener(mUsegeReminderSeekBarChangeListener);
mcbIsEnableUsegeReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "setIsEnableUsegeReminder");
mAppConfigUtils.setIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
//ControlCenterService.updateIsEnableUsegeReminder(mcbIsEnableUsegeReminder.isChecked());
}
});
// 初始化服务总开关
mswIsEnableService.setOnClickListener(new CompoundButton.OnClickListener() {
@Override
public void onClick(View view) {
mAppConfigUtils.setIsEnableService(getActivity(), mswIsEnableService.isChecked());
}
});
}
void setCurrentValueBattery(int value) {
//LogUtils.d(TAG, "setCurrentValueBattery");
mtvCurrentValue.setText(Integer.toString(value) + "%");
mCurrentValueBatteryDrawable.setValue(value);
mCurrentValueBatteryDrawable.invalidateSelf();
}
class ChargeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
//LogUtils.d(TAG, "call onProgressChanged");
int nChargeReminderValue = progress;
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
mChargeReminderValueBatteryDrawable.setValue(nChargeReminderValue);
mChargeReminderValueBatteryDrawable.invalidateSelf();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStartTrackingTouch");
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStopTrackingTouch");
//取得当前进度条的刻度
int nChargeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
mAppConfigUtils.setChargeReminderValue(nChargeReminderValue);
mtvChargeReminderValue.setText(Integer.toString(nChargeReminderValue) + "%");
//ControlCenterService.updateChargeReminderValue(nChargeReminderValue);
}
}
class UsegeReminderSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
//LogUtils.d(TAG, "call onProgressChanged");
int nUsegeReminderValue = progress;
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
mUsegeReminderValueBatteryDrawable.setValue(nUsegeReminderValue);
mUsegeReminderValueBatteryDrawable.invalidateSelf();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStartTrackingTouch");
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//LogUtils.d(TAG, "call onStopTrackingTouch");
//取得当前进度条的刻度
int nUsegeReminderValue = ((VerticalSeekBar)seekBar)._mnProgress;
LogUtils.d(TAG, "nUsegeReminderValue is " + Integer.toString(nUsegeReminderValue));
//LogUtils.d(TAG, "mPowerReminder is " + mApplication);
mAppConfigUtils.setUsegeReminderValue(nUsegeReminderValue);
//LogUtils.d(TAG, "opopopopopopopop");
mtvUsegeReminderValue.setText(Integer.toString(nUsegeReminderValue) + "%");
//ControlCenterService.updateUsegeReminderValue(nUsegeReminderValue);
}
}
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);
// }
}
Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_RELOAD_APPCONFIG : {
setViewData();
break;
}
case MSG_CURRENTVALUEBATTERY : {
setCurrentValueBattery(msg.arg1);
break;
}
}
super.handleMessage(msg);
}
};
public static void relaodAppConfigs() {
if (_mMainViewFragment != null) {
Handler handler = _mMainViewFragment.mHandler;
handler.sendMessage(handler.obtainMessage(MSG_RELOAD_APPCONFIG));
}
}
public static void sendMsgCurrentValueBattery(int value) {
if (_mMainViewFragment != null) {
Handler handler = _mMainViewFragment.mHandler;
Message msg = handler.obtainMessage(MSG_CURRENTVALUEBATTERY);
msg.arg1 = value;
handler.sendMessage(msg);
}
}
}

View File

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

View File

@@ -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.models.AppConfigBean;
import cc.winboll.studio.powerbell.beans.AppConfigBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;

View File

@@ -5,9 +5,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;
import cc.winboll.studio.powerbell.utils.NotificationHelper;
public class GlobalApplicationReceiver extends BroadcastReceiver {
@@ -47,7 +48,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
//NotificationHelper.cancelRemindNotification(context);
App.getAppCacheUtils(context).addChangingTime(nTheQuantityOfElectricity);
MainActivity.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
MainViewFragment.sendMsgCurrentValueBattery(nTheQuantityOfElectricity);
// 保存好新的电池状态标志
_mIsCharging = isCharging;
_mnTheQuantityOfElectricityOld = nTheQuantityOfElectricity;

View File

@@ -23,15 +23,15 @@ 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.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.services.AssistantService;
import cc.winboll.studio.powerbell.threads.RemindThread;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import cc.winboll.studio.powerbell.utils.NotificationHelper;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.utils.StringUtils;
@@ -48,7 +48,7 @@ public class ControlCenterService extends Service {
AppConfigUtils mAppConfigUtils;
AppCacheUtils mAppCacheUtils;
// 前台服务通知工具
NotificationManagerUtils mNotificationManagerUtils;
NotificationHelper mNotificationHelper;
Notification notification;
RemindThread mRemindThread;
ControlCenterServiceHandler mControlCenterServiceHandler;
@@ -72,7 +72,7 @@ public class ControlCenterService extends Service {
isServiceRunning = false;
mAppConfigUtils = App.getAppConfigUtils(this);
mAppCacheUtils = App.getAppCacheUtils(this);
mNotificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
mNotificationHelper = new NotificationHelper(ControlCenterService.this);
if (mMyServiceConnection == null) {
@@ -101,10 +101,10 @@ public class ControlCenterService extends Service {
wakeupAndBindAssistant();
// 显示前台通知栏
// 在Service中
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(this);
//Intent intent = new Intent(this, MainActivity.class);
notificationManagerUtils.startForegroundServiceNotify(ControlCenterService.this, new NotificationMessage(getString(R.string.app_name), "Service Running, Click to open app"));
//startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
NotificationHelper helper = new NotificationHelper(this);
Intent intent = new Intent(this, MainActivity.class);
notification = helper.showForegroundNotification(intent, getString(R.string.app_name), "Service Running, Click to open app");
startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
// NotificationMessage notificationMessage=createNotificationMessage();
// //Toast.makeText(getApplication(), "", Toast.LENGTH_SHORT).show();
@@ -260,9 +260,9 @@ public class ControlCenterService extends Service {
for (int i = 0; i < 20; i++) {
msg += szRemindMSG;
}
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
NotificationHelper helper = new NotificationHelper(ControlCenterService.this);
Intent intent = new Intent(ControlCenterService.this, MainActivity.class);
notificationManagerUtils.showTempAlertNotify(getString(R.string.app_name), msg);
helper.showTemporaryNotification(intent, getString(R.string.app_name), msg);

View File

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

View File

@@ -1,249 +1,39 @@
package cc.winboll.studio.powerbell.unittest;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.nfc.tech.TagTechnology;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* 终极修复版放弃FileProvider直接用私有目录File路径彻底解决UID冲突
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 18:04
* @Describe 单元测试启动主页窗口
*/
public class MainUnitTestActivity extends AppCompatActivity {
// ====================== 常量定义 ======================
public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0;
private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
// ====================== 成员变量移除所有Uri相关 ======================
private BackgroundView mBackgroundView;
private String mAppPrivateDirPath;
private File mPrivateTestImageFile; // 仅用File不用Uri
private File mPrivateCropImageFile;
BackgroundBean mPreviewBackgroundBean;
// ====================== 生命周期方法 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
initBaseParams();
initViewAndEvent();
copyAssetsTestImageToPrivateDir();
//loadBackgroundByFile(); // 直接用File加载
mPreviewBackgroundBean = new BackgroundBean();
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
doubleRefreshPreview();
ToastUtils.show("单元测试页面启动完成");
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "=== onActivityResult 回调 ===");
if (requestCode == REQUEST_CROP_IMAGE) {
handleCropResult(resultCode);
}
}
// ====================== 初始化相关方法 ======================
private void initBaseParams() {
LogUtils.d(TAG, "初始化基础参数:工具类+私有目录+File");
// 私有目录无需权限无UID冲突
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
File privateDir = new File(mAppPrivateDirPath);
if (!privateDir.exists()) {
privateDir.mkdirs();
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath);
}
// 初始化File无Uri
File refFile = new File(ASSETS_TEST_IMAGE_PATH);
String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png";
String uniqueCropName = uniqueTestName.replace(".png", "_crop.png");
mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName);
mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName);
LogUtils.d(TAG, "测试图File路径" + mPrivateTestImageFile.getAbsolutePath());
}
private void initViewAndEvent() {
LogUtils.d(TAG, "初始化布局与控件事件");
// 非调试状态就退出
if (!GlobalApplication.isDebugging()) {
finish();
}
setContentView(R.layout.activity_mainunittest);
mBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
// 跳转主页面按钮
Button btnMain = (Button) findViewById(R.id.btn_main_activity);
btnMain.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击按钮:跳转主页面");
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
}
});
// 裁剪按钮直接用File路径启动无Uri
Button btnCrop = (Button) findViewById(R.id.btn_test_cropimage);
btnCrop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击按钮启动裁剪File路径版");
ToastUtils.show("准备启动图片裁剪");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
startCropTestByFile(); // 直接传File
} else {
ToastUtils.show("测试图片未准备好,重新拷贝");
copyAssetsTestImageToPrivateDir();
}
}
});
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.activitymainunittestFrameLayout1, new BackgroundViewTestFragment(), BackgroundViewTestFragment.TAG);
fragmentTransaction.commit();
ToastUtils.show(String.format("%s onCreate", TAG));
}
// 从assets拷贝图片不变确保File存在
private void copyAssetsTestImageToPrivateDir() {
LogUtils.d(TAG, "开始拷贝assets图片到私有目录");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
LogUtils.d(TAG, "图片已存在,无需拷贝");
return;
}
InputStream inputStream = null;
try {
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节");
} catch (IOException e) {
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), e);
ToastUtils.show("图片准备失败");
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "关闭流失败:" + e.getMessage());
}
}
}
}
// ====================== 核心业务方法全改为File路径 ======================
/** 直接用File路径加载背景图无Uri无冲突 */
// private void loadBackgroundByFile() {
// LogUtils.d(TAG, "开始加载背景图File路径版");
// if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
// mBackgroundView.loadImage(mPrivateTestImageFile.getAbsolutePath()); // 直接传路径
// LogUtils.d(TAG, "背景图加载成功:" + mPrivateTestImageFile.getAbsolutePath());
// ToastUtils.show("背景图加载成功");
// } else {
// LogUtils.e(TAG, "背景图加载失败:文件无效");
// ToastUtils.show("背景图加载失败");
// }
// }
/** 直接用File启动裁剪关键调用ImageCropUtils的File重载方法 */
private void startCropTestByFile() {
LogUtils.d(TAG, "启动裁剪File路径版原图" + mPrivateTestImageFile.getAbsolutePath());
// 确保输出目录存在
File cropParent = mPrivateCropImageFile.getParentFile();
if (!cropParent.exists()) {
cropParent.mkdirs();
}
// 调用ImageCropUtils的File参数方法核心绕开Uri
ImageCropUtils.startImageCrop(
this,
mPrivateTestImageFile, // 原图File
mPrivateCropImageFile, // 输出File
0,
0,
true,
REQUEST_CROP_IMAGE
);
LogUtils.d(TAG, "裁剪请求已发送,输出路径:" + mPrivateCropImageFile.getAbsolutePath());
ToastUtils.show("已启动图片裁剪");
}
/** 处理裁剪结果直接校验输出File */
private void handleCropResult(int resultCode) {
LogUtils.d(TAG, "裁剪回调处理resultCode=" + resultCode);
if (resultCode == RESULT_OK) {
if (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) {
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
LogUtils.d(TAG, "裁剪成功,加载裁剪图:" + mPrivateCropImageFile.getAbsolutePath());
ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
doubleRefreshPreview();
} else {
LogUtils.e(TAG, "裁剪成功但输出文件无效");
ToastUtils.show("裁剪失败:输出文件无效");
}
} else if (resultCode == RESULT_CANCELED) {
LogUtils.d(TAG, "裁剪取消");
ToastUtils.show("裁剪已取消");
} else {
LogUtils.e(TAG, "裁剪失败resultCode异常");
ToastUtils.show("裁剪失败");
}
}
/**
* 双重刷新预览,确保背景加载最新数据
* 移除:缓存清空逻辑
*/
private void doubleRefreshPreview() {
// 第一重刷新
try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
return;
}
// 第二重刷新(延迟执行)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing()) {
try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
}
}
}
}, 200);
}
}

View File

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

View File

@@ -5,8 +5,8 @@ import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.beans.AppConfigBean;
import cc.winboll.studio.powerbell.beans.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.services.ControlCenterService;
@@ -79,7 +79,7 @@ public class AppConfigUtils {
@Override
public void onNo() {
MainActivity.relaodAppConfigs();
MainViewFragment.relaodAppConfigs();
}
});
}
@@ -183,7 +183,7 @@ public class AppConfigUtils {
@Override
public void onNo() {
AppConfigUtils.getInstance(activity).loadAppConfigBean();
MainActivity.relaodAppConfigs();
MainViewFragment.relaodAppConfigs();
}
});
}
@@ -198,6 +198,6 @@ public class AppConfigUtils {
AppConfigBean.saveBean(mContext, mAppConfigBean);
// 通知活动窗口和服务配置已更新
ControlCenterService.updateStatus(mContext, mAppConfigBean);
MainActivity.relaodAppConfigs();
MainViewFragment.relaodAppConfigs();
}
}

View File

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

View File

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

View File

@@ -1,703 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 12:07:20
* @Describe 背景图片工具集精简版复用FileUtils聚焦业务逻辑
*/
public class BackgroundSourceUtils {
public static final String TAG = "BackgroundSourceUtils";
// 裁剪相关常量(统一定义,避免硬编码)
private static final String CROP_CACHE_DIR_NAME = "cache";
private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg";
private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg";
public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider";
// 图片操作基础目录
private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell";
private static final String SOURCE_DIR_NAME = "BackgroundSource";
private static final String COMPRESS_DIR_NAME = "BackgroundCompress";
// 单例相关
private static volatile BackgroundSourceUtils sInstance;
private Context mContext;
private File currentBackgroundBeanFile;
private BackgroundBean currentBackgroundBean;
private File previewBackgroundBeanFile;
private BackgroundBean previewBackgroundBean;
// 目录文件相关
private File fPictureBaseDir;
private File fCropCacheDir;
private File fBackgroundSourceDir;
private File fBackgroundCompressDir;
private File fUtilsDir;
private File fModelDir;
private File mCropSourceFile;
private File mCropResultFile;
// 双重校验锁单例
private BackgroundSourceUtils(Context context) {
if (sInstance != null) {
throw new RuntimeException("BackgroundSourceUtils 是单例类,禁止重复创建!");
}
this.mContext = context.getApplicationContext();
initNecessaryDirs();
initAllFiles();
loadSettings();
}
public static BackgroundSourceUtils getInstance(Context context) {
if (sInstance == null) {
synchronized (BackgroundSourceUtils.class) {
if (sInstance == null) {
sInstance = new BackgroundSourceUtils(context);
}
}
}
return sInstance;
}
/**
* 统一初始化所有必要目录
*/
private void initNecessaryDirs() {
LogUtils.d(TAG, "【目录初始化】开始创建所有必要目录");
initPictureDirs();
initJsonDirs();
LogUtils.d(TAG, "【目录初始化】所有必要目录创建完成");
}
/**
* 初始化图片操作目录
*/
private void initPictureDirs() {
fPictureBaseDir = new File(PICTURE_BASE_DIR);
fBackgroundSourceDir = new File(fPictureBaseDir, SOURCE_DIR_NAME);
fCropCacheDir = new File(fPictureBaseDir, CROP_CACHE_DIR_NAME);
fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_DIR_NAME);
createDirWithPermission(fPictureBaseDir, "图片基础目录");
createDirWithPermission(fBackgroundSourceDir, "图片存储目录");
createDirWithPermission(fCropCacheDir, "裁剪缓存目录");
createDirWithPermission(fBackgroundCompressDir, "压缩图存储目录");
validatePictureDirs();
}
/**
* 初始化JSON配置目录
*/
private void initJsonDirs() {
fUtilsDir = mContext.getExternalFilesDir(TAG);
if (fUtilsDir == null) {
LogUtils.e(TAG, "应用外置存储不可用,切换到应用内部缓存目录");
fUtilsDir = mContext.getDataDir();
}
fModelDir = new File(fUtilsDir, "ModelDir");
createDirWithPermission(fModelDir, "JSON配置目录");
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
}
/**
* 创建目录并校验
*/
private void createDirWithPermission(File dir, String dirDesc) {
if (dir == null) {
LogUtils.e(TAG, dirDesc + "创建失败目录对象为null");
return;
}
if (!dir.exists()) {
LogUtils.d(TAG, dirDesc + "不存在,开始创建:" + dir.getAbsolutePath());
dir.mkdirs();
}
}
/**
* 校验图片目录是否就绪
*/
private void validatePictureDirs() {
boolean allReady = fPictureBaseDir.exists() && fBackgroundSourceDir.exists()
&& fCropCacheDir.exists() && fBackgroundCompressDir.exists();
if (allReady) {
LogUtils.d(TAG, "所有图片目录均已就绪");
} else {
LogUtils.e(TAG, "部分图片目录未就绪,可能影响后续功能");
}
}
/**
* 初始化所有文件
*/
private void initAllFiles() {
clearCropTempFiles();
LogUtils.d(TAG, "文件初始化完成");
}
/**
* 将File转为ContentUri
*/
public Uri getFileProviderUri(File file) {
LogUtils.d(TAG, "【getFileProviderUri调用】文件路径" + file.getAbsolutePath());
Uri contentUri = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
contentUri = FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITY, file);
LogUtils.d(TAG, "7.0+ 生成ContentUri" + contentUri.toString());
} else {
contentUri = Uri.fromFile(file);
LogUtils.d(TAG, "7.0以下 生成FileUri" + contentUri.toString());
}
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "生成Uri失败" + e.getMessage(), e);
contentUri = null;
}
return contentUri;
}
/**
* 检查背景是否为空并创建空白背景Bean
*/
boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) {
LogUtils.d(TAG, "【checkEmptyBackgroundAndCreateBlankBackgroundBean调用】开始检查背景Bean");
File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath());
if (!fCheckBackgroundFile.exists()) {
String newCropFileName = genNewCropFileName();
String fileSuffix = "png";
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
AssetsCopyUtils.copyAssetsFileToDir(mContext, "images/blank10x10.png", mCropSourceFile.getAbsolutePath());
try {
mCropResultFile.createNewFile();
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
loadSettings();
previewBackgroundBean.setIsUseBackgroundFile(true);
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(false);
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
saveSettings();
LogUtils.d(TAG, "背景Bean为空已创建空白背景并更新配置");
return true;
}
LogUtils.d(TAG, "背景Bean文件存在无需创建空白背景");
return false;
}
String genNewCropFileName() {
return UUID.randomUUID().toString() + System.currentTimeMillis();
}
/**
* 创建并更新预览剪裁环境
*/
public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) {
LogUtils.d(TAG, "【createAndUpdatePreviewEnvironmentForCropping调用】开始初始化预览剪裁环境");
InputStream is = null;
FileOutputStream fos = null;
try {
clearCropTempFiles();
if (checkEmptyBackgroundAndCreateBlankBackgroundBean(oldPreviewBackgroundBean)) {
LogUtils.d(TAG, "空白背景创建成功,直接返回");
return true;
}
Uri uri = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
LogUtils.d(TAG, String.format("createAndUpdatePreviewEnvironmentForCropping: uri %s", uri));
String fileSuffix = UriUtils.getSuffixFromUri(mContext, uri);
LogUtils.d(TAG, String.format("createAndUpdatePreviewEnvironmentForCropping: fileSuffix = %s", fileSuffix));
String newCropFileName = genNewCropFileName();
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + ".png");
if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) {
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile);
} else {
mCropResultFile.createNewFile();
}
if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundFilePath())) {
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile);
} else {
mCropSourceFile.createNewFile();
is = mContext.getContentResolver().openInputStream(uri);
if (is == null) {
LogUtils.e(TAG, "ContentResolver打开Uri失败" + uri.toString());
return false;
}
fos = new FileOutputStream(mCropSourceFile);
byte[] buffer = new byte[1024 * 8];
int readLen;
while ((readLen = is.read(buffer)) != -1) {
fos.write(buffer, 0, readLen);
}
fos.flush();
try {
fos.getFD().sync();
} catch (IOException e) {
LogUtils.w(TAG, "文件同步到磁盘失败flush兜底" + e.getMessage());
fos.flush();
}
}
loadSettings();
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
saveSettings();
LogUtils.d(TAG, "预览剪裁环境初始化成功");
LogUtils.d(TAG, "→ 原Uri" + uri.toString());
LogUtils.d(TAG, "→ 剪裁数据源:" + mCropSourceFile.getAbsolutePath());
LogUtils.d(TAG, "→ 剪裁结果文件:" + mCropResultFile.getAbsolutePath());
return true;
} catch (Exception e) {
LogUtils.e(TAG, "预览剪裁环境初始化异常:" + e.getMessage(), e);
clearCropTempFiles();
return false;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage());
}
}
}
}
/**
* 加载背景配置
*/
public void loadSettings() {
currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
if (currentBackgroundBean == null) {
currentBackgroundBean = new BackgroundBean();
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
LogUtils.d(TAG, "正式背景Bean不存在已创建新实例");
}
previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
if (previewBackgroundBean == null) {
previewBackgroundBean = new BackgroundBean();
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "预览背景Bean不存在已创建新实例");
}
}
// ------------------------------ 对外提供的核心方法 ------------------------------
public BackgroundBean getCurrentBackgroundBean() {
return currentBackgroundBean;
}
public BackgroundBean getPreviewBackgroundBean() {
return previewBackgroundBean;
}
public String getPreviewBackgroundScaledCompressFilePath() {
String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
LogUtils.e(TAG, "预览压缩背景文件名为空");
return "";
}
File file = new File(fBackgroundCompressDir, compressFileName);
return file.getAbsolutePath();
}
public String getCurrentBackgroundScaledCompressFilePath() {
String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
LogUtils.e(TAG, "正式压缩背景文件名为空");
return "";
}
File file = new File(fBackgroundCompressDir, compressFileName);
return file.getAbsolutePath();
}
/**
* 保存配置
*/
public void saveSettings() {
if (currentBackgroundBean != null && previewBackgroundBean != null) {
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "两份背景配置保存成功");
} else {
LogUtils.e(TAG, "配置保存失败current/preview Bean存在空值");
}
}
public String getBackgroundSourceDirPath() {
return fBackgroundSourceDir.getAbsolutePath();
}
public String getBackgroundCompressDirPath() {
return fBackgroundCompressDir.getAbsolutePath();
}
public String getCropCacheDir() {
return fCropCacheDir.getAbsolutePath();
}
public String getFileProviderAuthority() {
return FILE_PROVIDER_AUTHORITY;
}
// ------------------------------ 核心业务方法 ------------------------------
/**
* 保存裁剪结果图到预览Bean
*/
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
LogUtils.d(TAG, "【saveFileToPreviewBean调用】源文件路径" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) {
LogUtils.e(TAG, "源文件无效,拒绝保存");
return previewBackgroundBean;
}
String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath();
if (sourceFile.getAbsolutePath().contains(originalImageDir)) {
LogUtils.w(TAG, "禁止复制原图,跳过保存");
return previewBackgroundBean;
}
if (!fBackgroundSourceDir.exists() && !fBackgroundSourceDir.mkdirs()) {
LogUtils.e(TAG, "BackgroundSource目录创建失败");
return previewBackgroundBean;
}
String uniqueFileName = "bg_" + System.currentTimeMillis() + "_" + sourceFile.getName();
File targetFile = new File(fBackgroundSourceDir, uniqueFileName);
if (FileUtils.copyFile(sourceFile, targetFile)) {
LogUtils.d(TAG, "裁剪结果图保存成功:" + targetFile.getAbsolutePath());
previewBackgroundBean.setBackgroundFileName(uniqueFileName);
previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath());
previewBackgroundBean.setBackgroundFileInfo(fileInfo);
previewBackgroundBean.setIsUseBackgroundFile(true);
saveSettings();
} else {
LogUtils.e(TAG, "裁剪结果图复制失败");
}
return previewBackgroundBean;
}
/**
* 提交预览背景到正式背景
*/
public void commitPreviewSourceToCurrent() {
LogUtils.d(TAG, "【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean");
//ToastUtils.show("【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean");
currentBackgroundBean = new BackgroundBean();
currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName());
currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath());
currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo());
currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile());
currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName());
currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath());
currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile());
currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth());
currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight());
currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor());
String previewFileName = previewBackgroundBean.getBackgroundFileName();
String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
File previewFile = new File(previewBackgroundBean.getBackgroundFilePath());
File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath());
File currentFile = new File(fBackgroundSourceDir, previewFileName);
File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName);
FileUtils.copyFile(previewFile, currentFile);
FileUtils.copyFile(previewCropFile, currentCropFile);
currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath());
currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath());
saveSettings();
LogUtils.d(TAG, "预览背景提交到正式背景成功,两份实例完全独立");
ToastUtils.show("背景图片应用成功");
}
/**
* 将正式背景同步到预览背景
*/
public void setCurrentSourceToPreview() {
LogUtils.d(TAG, "【setCurrentSourceToPreview调用】开始深拷贝正式Bean到预览Bean");
previewBackgroundBean = new BackgroundBean();
previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName());
previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath());
previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo());
previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile());
previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath());
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile());
previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth());
previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight());
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
saveSettings();
LogUtils.d(TAG, "正式背景同步到预览背景成功");
}
/**
* 清理单个旧文件
*/
private void clearOldFile(File file, String fileDesc) {
if (file == null) {
return;
}
if (file.exists()) {
file.delete();
LogUtils.d(TAG, fileDesc + "已删除");
}
}
/**
* 清理裁剪临时文件
*/
void clearCropTempFiles() {
File[] files = fCropCacheDir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
clearOldFile(file, "旧裁剪缓存文件:" + file.getAbsolutePath());
}
mCropSourceFile = null;
mCropResultFile = null;
}
/**
* 复制文件
*/
public boolean copyFile(File source, File target) {
LogUtils.d(TAG, "【copyFile调用】源文件" + (source != null ? source.getAbsolutePath() : "null") + " 目标:" + (target != null ? target.getAbsolutePath() : "null"));
if (source == null || TextUtils.isEmpty(source.getPath()) || (source.exists() && source.length() <= 0)) {
if (target == null) {
LogUtils.e(TAG, "目录创建失败目标对象为null");
return false;
}
File targetDir = target.isFile() ? target.getParentFile() : target;
createDirWithPermission(targetDir, "空源文件场景-目录创建");
LogUtils.d(TAG, "空源文件场景,目录创建完成");
return true;
}
return FileUtils.copyFile(source, target);
}
/**
* 获取目录类型描述
*/
public String getDirTypeDesc(File dir) {
if (dir == null) {
return "未知目录null";
}
String dirPath = dir.getAbsolutePath();
String publicPicturePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath();
String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : "";
String cachePath = mContext.getCacheDir().getAbsolutePath();
if (!TextUtils.isEmpty(publicPicturePath)) {
if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_DIR_NAME)) {
return "系统公共图片目录(/Pictures/PowerBell/BackgroundCompress压缩图统一存储目录";
} else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) {
return "系统公共图片目录(/Pictures/PowerBell图片存储/裁剪目录)";
}
} else if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) {
return "应用私有外部目录getExternalFilesDir()JSON配置目录";
} else if (dirPath.contains(cachePath)) {
return "应用内部缓存目录getCacheDir(),兜底目录)";
} else {
return "外部存储目录(非应用私有,权限受限)";
}
return "未知目录";
}
/**
* 迁移旧压缩图路径到新目录
*/
private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) {
LogUtils.d(TAG, "【migrateCompressPathToNewDir调用】开始迁移" + (isCurrentBean ? "正式" : "预览") + "Bean压缩路径");
String oldCompressPath = bean.getBackgroundScaledCompressFilePath();
String beanType = isCurrentBean ? "正式Bean" : "预览Bean";
if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) {
LogUtils.d(TAG, beanType + "无需迁移:旧路径为空或已在目标目录");
return;
}
File oldCompressFile = new File(oldCompressPath);
if (!oldCompressFile.exists() || !oldCompressFile.isFile() || oldCompressFile.length() <= 0) {
LogUtils.w(TAG, beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath);
String compressFileName = bean.getBackgroundScaledCompressFileName();
if (!TextUtils.isEmpty(compressFileName)) {
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
saveSettings();
LogUtils.d(TAG, beanType + "压缩路径已重置到目标目录");
}
return;
}
String compressFileName = bean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg";
}
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
boolean copySuccess = FileUtils.copyFile(oldCompressFile, newCompressFile);
if (copySuccess) {
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
saveSettings();
clearOldFile(oldCompressFile, beanType + "旧压缩文件(迁移后清理)");
LogUtils.d(TAG, beanType + "压缩路径迁移成功:" + oldCompressPath + "" + newCompressFile.getAbsolutePath());
} else {
LogUtils.e(TAG, beanType + "压缩文件复制失败,迁移终止");
}
}
/**
* 获取图片旋转角度
*/
public int getImageRotateAngle(String imagePath) {
LogUtils.d(TAG, "【getImageRotateAngle调用】图片路径" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "图片路径为空");
return 0;
}
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "图片文件无效:" + imagePath);
return 0;
}
InputStream inputStream = null;
try {
inputStream = new FileInputStream(imageFile);
ExifInterface exifInterface = new ExifInterface(inputStream);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default:
return 0;
}
} catch (IOException e) {
LogUtils.w(TAG, "读取EXIF异常" + e.getMessage());
return 0;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "流关闭失败:" + e.getMessage());
}
}
}
}
/**
* 压缩图片并保存(默认路径)
*/
public void compressQualityToRecivedPicture(Bitmap bitmap) {
LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】使用默认路径压缩图片");
String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath();
compressQualityToRecivedPicture(bitmap, defaultCompressPath);
}
/**
* 压缩图片并保存(指定路径)
*/
public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) {
LogUtils.d(TAG, "【compressQualityToRecivedPicture调用】指定路径压缩图片目标路径" + targetCompressPath);
if (bitmap == null || bitmap.isRecycled()) {
ToastUtils.show("压缩失败:图片为空");
LogUtils.e(TAG, "Bitmap为空或已回收");
return;
}
OutputStream outStream = null;
FileOutputStream fos = null;
try {
LogUtils.d(TAG, "Bitmap原始大小" + bitmap.getByteCount() / 1024 + "KB");
File targetCompressFile = new File(targetCompressPath);
if (targetCompressFile.exists()) {
targetCompressFile.delete();
}
targetCompressFile.createNewFile();
fos = new FileOutputStream(targetCompressFile);
outStream = new BufferedOutputStream(fos);
boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
outStream.flush();
try {
fos.getFD().sync();
LogUtils.d(TAG, "图片已强制同步到磁盘");
} catch (IOException e) {
LogUtils.w(TAG, "sync失败flush兜底" + e.getMessage());
outStream.flush();
}
LogUtils.d(TAG, "图片压缩" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB");
ToastUtils.show(compressSuccess ? "图片压缩成功" : "图片压缩失败");
} catch (IOException e) {
LogUtils.e(TAG, "图片压缩IO异常" + e.getMessage(), e);
ToastUtils.show("图片压缩失败");
} finally {
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "BufferedOutputStream关闭失败" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "FileOutputStream关闭失败" + e.getMessage());
}
}
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
}

View File

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

View File

@@ -1,101 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/08 21:11
* @Describe 把 R.drawable 中的图片保存为 File 对象的工具类
* 适配 PowerBell 项目支持指定保存路径、自动创建目录、处理PNG图片压缩
*/
public class DrawableToFileUtils {
private static final String TAG = "DrawableToFileUtils";
/**
* 核心方法:将 R.drawable 图片保存为 File 对象
* @param context 上下文(用于获取 Resources
* @param drawableResId 图片资源ID如 R.drawable.ic_test_png
* @param fileName 保存的文件名(需带 .png 后缀,如 "test_drawable.png"
* @return 保存成功返回 File 对象,失败返回 null
*/
public static File saveDrawableToFile(Context context, int drawableResId, String filePath) {
// 1. 校验参数(避免空指针/无效参数)
if (context == null || drawableResId == 0 || filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "【保存失败】参数无效context为空/资源ID为0/文件名为空)");
return null;
}
if (!filePath.endsWith(".png")) {
filePath += ".png"; // 强制添加 .png 后缀,确保图片格式正确
LogUtils.d(TAG, "【格式适配】自动添加.png后缀最终文件名" + filePath);
}
// 3. 构建目标 File 对象(最终保存的文件路径)
File targetFile = new File(filePath);
LogUtils.d(TAG, "【保存路径】目标文件路径:" + targetFile.getAbsolutePath());
// 4. 读取 drawable 资源为 Bitmap处理高清图/缩放问题)
Bitmap bitmap = null;
try {
// 读取 drawable 资源(适配不同分辨率的图片,避免变形)
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
if (bitmap == null) {
LogUtils.e(TAG, "【读取失败】无法读取drawable资源资源ID" + drawableResId + "");
return null;
}
LogUtils.d(TAG, "【读取成功】drawable资源转Bitmap成功" + bitmap.getWidth() + ",高:" + bitmap.getHeight() + "");
// 5. 将 Bitmap 写入 FilePNG格式无损保存
FileOutputStream fos = new FileOutputStream(targetFile);
// 压缩参数PNG格式质量100无损写入输出流
boolean isSaved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush(); // 刷新输出流
fos.close(); // 关闭输出流
// 6. 校验保存结果(文件是否存在且有效)
if (isSaved && targetFile.exists() && targetFile.length() > 100) {
LogUtils.d(TAG, "【保存成功】drawable图片保存为File" + targetFile.getAbsolutePath());
return targetFile; // 保存成功返回File对象
} else {
LogUtils.e(TAG, "【保存失败】图片写入文件无效(文件大小:" + (targetFile.exists() ? targetFile.length() : 0) + "字节)");
// 保存失败,删除无效文件
if (targetFile.exists()) {
targetFile.delete();
LogUtils.d(TAG, "【清理无效文件】已删除无效文件:" + targetFile.getAbsolutePath());
}
return null;
}
} catch (IOException e) {
LogUtils.e(TAG, "【保存异常】写入文件时出错:" + e.getMessage());
LogUtils.e(TAG, "【异常堆栈】" + Log.getStackTraceString(e));
return null;
} finally {
// 回收Bitmap资源避免内存溢出
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
LogUtils.d(TAG, "【资源回收】Bitmap资源已回收");
}
}
}
/**
* 重载方法:自定义保存路径(灵活适配不同场景)
* @param context 上下文
* @param drawableResId 图片资源ID
* @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/"
* @param fileName 保存的文件名(带.png后缀
* @return 保存成功返回File对象失败返回null
*/
public static File saveDrawableToFile(Context context, int drawableResId, String saveDirPath, String fileName) {
File filePath = new File(saveDirPath, fileName);
return saveDrawableToFile(context, drawableResId, filePath.getAbsolutePath());
}
}

View File

@@ -1,290 +1,176 @@
package cc.winboll.studio.powerbell.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.*;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import android.content.Context;
import android.net.Uri;
/**
* 文件操作工具类
* 功能:文件读写、复制、图片转换、文件名处理等常用文件操作
* 适配Java 7+支持Android全版本
* 注意调用文件操作前需确保已获取存储权限Android 6.0+ 需动态申请)
* 文件读取工具类
*/
public class FileUtils {
/** 日志标签 */
public static final String TAG = "FileUtils";
/** 读取文件默认缓冲区大小10KB */
private static final int BUFFER_SIZE = 10240;
/** 最大读取文件大小1GB防止OOM */
private static final long MAX_READ_FILE_SIZE = 1024 * 1024 * 1024;
// ====================================== 文件读取相关 ======================================
/**
* 读取文件内容并转为字符串
* @param filePath 文件绝对路径(非空)
* @return 文件内容字符串
* @throws IOException 异常:文件不存在、文件过大、读取失败等
*/
//
// 读取文件内容,作为字符串返回
//
public static String readFileAsString(String filePath) throws IOException {
// 1. 校验文件合法性
File file = new File(filePath);
if (!file.exists()) {
throw new FileNotFoundException("文件不存在:" + filePath);
}
if (file.length() > MAX_READ_FILE_SIZE) {
throw new IOException("文件过大超过1GB禁止读取" + filePath);
}
throw new FileNotFoundException(filePath);
}
// 2. 读取文件内容使用StringBuilder高效拼接
StringBuilder sb = new StringBuilder((int) file.length());
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
int readLen;
// 循环读取缓冲区避免一次性读取大文件导致OOM
while ((readLen = fis.read(buffer)) > 0) {
sb.append(new String(buffer, 0, readLen));
}
}
if (file.length() > 1024 * 1024 * 1024) {
throw new IOException("File is too large");
}
StringBuilder sb = new StringBuilder((int) (file.length()));
// 创建字节输入流
FileInputStream fis = new FileInputStream(filePath);
// 创建一个长度为10240的Buffer
byte[] bbuf = new byte[10240];
// 用于保存实际读取的字节数
int hasRead = 0;
while ((hasRead = fis.read(bbuf)) > 0) {
sb.append(new String(bbuf, 0, hasRead));
}
fis.close();
return sb.toString();
}
/**
* 读取文件内容并转为byte数组适用于二进制文件图片、音频等
* @param filePath 文件绝对路径(非空)
* @return 文件内容byte数组
* @throws IOException 异常:文件不存在、读取失败等
*/
//
// 根据文件路径读取byte[] 数组
//
public static byte[] readFileByBytes(String filePath) throws IOException {
// 1. 校验文件合法性
File file = new File(filePath);
if (!file.exists()) {
throw new FileNotFoundException("文件不存在:" + filePath);
}
throw new FileNotFoundException(filePath);
} else {
ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
BufferedInputStream in = null;
// 2. 缓冲流读取高效减少IO次数
try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
try {
in = new BufferedInputStream(new FileInputStream(file));
short bufSize = 1024;
byte[] buffer = new byte[bufSize];
int len1;
while (-1 != (len1 = in.read(buffer, 0, bufSize))) {
bos.write(buffer, 0, len1);
}
byte[] buffer = new byte[BUFFER_SIZE];
int readLen;
while ((readLen = bis.read(buffer)) != -1) {
bos.write(buffer, 0, readLen);
byte[] var7 = bos.toByteArray();
return var7;
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException var14) {
var14.printStackTrace();
}
bos.close();
}
bos.flush();
return bos.toByteArray();
}
}
// ====================================== 文件复制相关 ======================================
/**
* 基于FileChannel复制文件高效适用于大文件复制
* @param source 源文件(非空,必须存在)
* @param dest 目标文件(非空,父目录会自动创建)
* @throws IOException 异常:源文件不存在、复制失败等
*/
//
// 文件复制函数
//
public static void copyFileUsingFileChannels(File source, File dest) throws IOException {
// 1. 校验源文件合法性
if (!source.exists() || !source.isFile()) {
throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath());
}
// 2. 创建目标文件父目录
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
// 3. 通道复制try-with-resources 自动关闭通道,无需手动关闭)
try (FileChannel inputChannel = new FileInputStream(source).getChannel();
FileChannel outputChannel = new FileOutputStream(dest).getChannel()) {
// 从输入通道复制到输出通道(高效,底层优化)
FileChannel inputChannel = null;
FileChannel outputChannel = null;
try {
inputChannel = new FileInputStream(source).getChannel();
outputChannel = new FileOutputStream(dest).getChannel();
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
LogUtils.d(TAG, "文件复制成功FileChannel" + source.getAbsolutePath() + "" + dest.getAbsolutePath());
} finally {
inputChannel.close();
outputChannel.close();
}
}
/**
* 简化版文件复制基于NIO Files工具类代码简洁适用于中小文件
* @param oldFile 源文件(非空,必须存在)
* @param newFile 目标文件(非空,父目录会自动创建)
* @return 复制结果true-成功false-失败
* 将文件生成位图
* @param path
* @return
* @throws IOException
*/
public static boolean copyFile(File oldFile, File newFile) {
// 1. 校验源文件合法性
if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) {
LogUtils.e(TAG, "源文件无效:" + (oldFile != null ? oldFile.getAbsolutePath() : "null"));
return false;
public static BitmapDrawable getImageDrawable(String path)
throws IOException {
//打开文件
File file = new File(path);
if (!file.exists()) {
return null;
}
// 2. 创建目标文件父目录
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
int BUFFER_SIZE = 1000;
byte[] bt = new byte[BUFFER_SIZE];
//得到文件的输入流
InputStream in = new FileInputStream(file);
//将文件读出到输出流中
int readLength = in.read(bt);
while (readLength != -1) {
outStream.write(bt, 0, readLength);
readLength = in.read(bt);
}
//转换成byte 后 再格式化成位图
byte[] data = outStream.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);// 生成位图
BitmapDrawable bd = new BitmapDrawable(bitmap);
return bd;
}
public static boolean copyFile(File oldFile, File newFile) {
//String oldPath = "path/to/original/file.txt";
//String newPath = "path/to/new-location/for/file.txt";
//File oldFile = new java.io.File(oldPath);
//File newFile = new java.io.File(newPath);
if (!newFile.getParentFile().exists()) {
newFile.getParentFile().mkdirs();
}
// 3. 复制文件(覆盖已有目标文件)
try {
Path sourcePath = Paths.get(oldFile.getPath());
Path destPath = Paths.get(newFile.getPath());
// 先删除已有目标文件(避免覆盖失败)
if (newFile.exists()) {
newFile.delete();
}
Files.copy(sourcePath, destPath);
LogUtils.d(TAG, "文件复制成功Files" + oldFile.getAbsolutePath() + "" + newFile.getAbsolutePath());
return true;
} catch (Exception e) {
LogUtils.e(TAG, "文件复制失败:" + e.getMessage(), e);
return false;
}
}
// ====================================== 图片文件相关 ======================================
/**
* 从文件路径获取BitmapDrawable适用于Android图片显示
* @param path 图片文件绝对路径(非空)
* @return BitmapDrawable 图片对象(文件不存在/读取失败返回null
* @throws IOException 异常文件读取IO错误
*/
public static BitmapDrawable getImageDrawable(String path) throws IOException {
// 1. 校验文件合法性
File file = new File(path);
if (!file.exists() || !file.isFile()) {
LogUtils.e(TAG, "图片文件不存在:" + path);
return null;
}
// 2. 读取文件并转为BitmapDrawable缓冲流读取减少内存占用
try (InputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
int readLen;
while ((readLen = is.read(buffer)) != -1) {
bos.write(buffer, 0, readLen);
}
// 3. 生成Bitmap并包装为BitmapDrawable
byte[] imageBytes = bos.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
return new BitmapDrawable(bitmap);
}
}
// ====================================== 文件名处理相关 ======================================
/**
* 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png"
* @param file 目标文件可为null
* @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串
*/
public static String getFileSuffixWithMultiDot(File file) {
// 1. 校验文件合法性
if (file == null || !file.isFile()) {
return "";
}
// 2. 提取文件名并查找最后一个 "."
String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件)
if (lastDotIndex == -1 // 无 "."
|| lastDotIndex == fileName.length() - 1 // 以 "." 结尾(如".gitignore"
|| (fileName.length() - lastDotIndex) > 5) { // 后缀长度超过5异常文件名
return "";
}
// 4. 返回小写后缀(统一格式,避免大小写不一致问题)
return fileName.substring(lastDotIndex).toLowerCase();
}
/**
* 生成唯一文件名(优化版:唯一、合法、简洁)
* 生成规则UUID(去掉"-") + "_" + 时间戳 + 原文件后缀
* @param refFile 参考文件用于提取后缀名可为null
* @return 唯一文件名(如"a1b2c3d4e5f6_1730000000000.jpg",无后缀则不带点)
*/
public static String createUniqueFileName(File refFile) {
// 1. 获取参考文件的后缀名自动容错null/无效文件)
String suffix = getFileSuffixWithMultiDot(refFile);
// 2. 生成唯一标识UUID确保全局唯一时间戳进一步降低重复概率
String uniqueId = UUID.randomUUID().toString().replace("-", ""); // 去掉"-"简化文件名
long timeStamp = System.currentTimeMillis();
// 3. 拼接文件名(分场景处理,避免多余点)
if (suffix.isEmpty()) {
// 无后缀唯一ID + 时间戳
return String.format("%s_%d", uniqueId, timeStamp);
if (!oldFile.exists()) {
//System.out.println("The original file does not exist.");
LogUtils.d(TAG, "The original file does not exist.");
} else {
// 有后缀唯一ID + 时间戳 + 后缀(无多余点)
return String.format("%s_%d%s", uniqueId, timeStamp, suffix);
try {
// 源文件路径
Path sourcePath = Paths.get(oldFile.getPath());
// 目标文件路径
Path destPath = Paths.get(newFile.getPath());
if(newFile.exists()) {
newFile.delete();
}
Files.copy(sourcePath, destPath);
LogUtils.d(TAG, "File copy successfully.");
//System.out.println("File moved successfully.");
return true;
} catch (Exception e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//System.err.println("An error occurred while moving the file: " + e.getMessage());
}
}
return false;
}
/**
* 复制输入流到文件兼容Uri解析失败场景
*/
public static void copyStreamToFile(InputStream inputStream, File file) throws IOException {
if (inputStream == null || file == null) {
throw new IllegalArgumentException("InputStream或File不能为空");
}
File parentDir = file.getParentFile();
if (!parentDir.exists() && !parentDir.mkdirs()) {
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath());
}
try {
OutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
} finally {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e("FileUtils", "关闭输入流失败:" + e.getMessage());
}
}
}
public static boolean isFileExists(String path) {
File file = new File(path);
return file.exists();
}
/**
* 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
* @param file 目标文件
* @return 后缀字符串(无后缀返回空字符串,非空统一小写)
*/
public static String getFileSuffix(File file) {
if (file == null || file.getName().isEmpty()) {
return ""; // 空文件/空文件名,返回空
}
String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 无后缀(没有点,或点在开头/结尾)
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
return "";
}
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
return fileName.substring(lastDotIndex + 1).toLowerCase();
}
}

View File

@@ -1,302 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import com.yalantis.ucrop.UCrop;
import java.io.File;
/**
* 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File 双传参)
*/
public class ImageCropUtils {
public static final String TAG = "ImageCropUtils";
// FileProvider 授权(与 AndroidManifest 配置一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// 强制输出格式:固定为 PNG保留透明通道
private static final String FORCE_OUTPUT_SUFFIX = "png";
private static final android.graphics.Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = android.graphics.Bitmap.CompressFormat.PNG;
// ====================== 核心裁剪方法(强制 PNG 输出,优化逻辑)======================
/**
* 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
* @param inputUri 输入图片 Uri本应用 FileProvider Uri非空
* @param outputUri 输出图片 Uri本应用 FileProvider Uri非空
* @param aspectX 固定比例 X自由裁剪传 0
* @param aspectY 固定比例 Y自由裁剪传 0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
public static void startImageCrop(Activity activity,
Uri inputUri,
Uri outputUri,
int aspectX,
int aspectY,
boolean isFreeCrop,
int requestCode) {
// 1. 输入参数校验
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
return;
}
if (inputUri == null || outputUri == null) {
LogUtils.e(TAG, "【裁剪异常】输入/输出 Uri 为空");
showToast(activity, "图片 Uri 无效,无法裁剪");
return;
}
if (!isValidUri(activity, inputUri)) {
LogUtils.e(TAG, "【裁剪异常】输入 Uri 无效:" + inputUri);
showToast(activity, "原图 Uri 无效,无法裁剪");
return;
}
// 2. 核心:强制修正输出为 PNG忽略原图格式统一转 PNG
File outputFile = uriToFile(activity, outputUri);
if (outputFile == null) {
LogUtils.e(TAG, "【裁剪异常】输出 Uri 转 File 失败:" + outputUri);
showToast(activity, "裁剪输出路径无效");
return;
}
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
// 4. 启动裁剪
uCrop.withOptions(options);
uCrop.start(activity, requestCode);
LogUtils.d(TAG, "【裁剪启动成功Uri 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
}
/**
* 【File 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
* @param inputFile 输入图片文件(任意格式)
* @param outputFile 输出图片文件(最终强制转为 PNG
* @param aspectX 固定比例 X自由裁剪传 0
* @param aspectY 固定比例 Y自由裁剪传 0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
public static void startImageCrop(Activity activity,
File inputFile,
File outputFile,
int aspectX,
int aspectY,
boolean isFreeCrop,
int requestCode) {
// 1. 输入参数校验
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
return;
}
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
LogUtils.e(TAG, "【裁剪异常】输入图片文件无效");
showToast(activity, "无有效图片可裁剪");
return;
}
if (outputFile == null) {
LogUtils.e(TAG, "【裁剪异常】输出文件路径为空");
showToast(activity, "裁剪输出路径无效");
return;
}
// 2. 核心:强制修正输出为 PNG忽略原图格式
Uri inputUri = getFileProviderUri(activity, inputFile);
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
Uri outputUri = getFileProviderUri(activity, outputFile);
// 3. 初始化 uCrop + 强制 PNG 配置
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
// 4. 启动裁剪
uCrop.withOptions(options);
uCrop.start(activity, requestCode);
LogUtils.d(TAG, "【裁剪启动成功File 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
}
/**
* 【BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
*/
public static void startImageCrop(Activity activity,
BackgroundBean cropBean,
int aspectX,
int aspectY,
boolean isFreeCrop,
int requestCode) {
File inputFile = new File(cropBean.getBackgroundFilePath());
File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath());
startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
}
// ====================== 裁剪结果处理(保持兼容,优化日志)======================
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
if (requestCode != cropRequestCode) return null;
if (resultCode == Activity.RESULT_OK && data != null) {
Uri outputUri = UCrop.getOutput(data);
if (outputUri != null) {
String outputPath = uriToPath(outputUri);
LogUtils.d(TAG, "【裁剪成功】强制输出 PNG透明保留输出路径" + outputPath);
return outputPath;
}
} else if (resultCode == UCrop.RESULT_ERROR) {
Throwable error = UCrop.getError(data);
LogUtils.e(TAG, "【裁剪失败】原因:" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【裁剪取消】用户手动取消");
}
return null;
}
// ====================== 辅助方法(优化适配强制 PNG 逻辑)======================
/** 校验 Uri 有效性(确保是图片类型) */
private static boolean isValidUri(Activity activity, Uri uri) {
try {
String type = activity.getContentResolver().getType(uri);
return type != null && type.startsWith("image/");
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 校验失败】原因:" + e.getMessage());
return false;
}
}
/** Uri 转 File适配 FileProvider Uri 和普通 Uri */
private static File uriToFile(Activity activity, Uri uri) {
if (uri == null) return null;
try {
if (uri.getScheme().equals("file")) {
return new File(uri.getPath());
}
String filePath = uri.getPath();
if (filePath == null) return null;
if (filePath.contains("/external_files/")) {
filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/");
} else if (filePath.contains("/cache/")) {
filePath = filePath.replace("/cache/", activity.getCacheDir().getAbsolutePath() + "/");
}
return new File(filePath);
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 转 File 失败】uri=" + uri + ",原因:" + e.getMessage());
return null;
}
}
/** Uri 提取文件路径 */
private static String uriToPath(Uri uri) {
if (uri == null) return null;
try {
if (uri.getScheme().equals("file")) {
return uri.getPath();
}
String path = uri.getPath();
if (path == null) return null;
String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"};
for (String prefix : prefixes) {
if (path.contains(prefix)) {
path = path.substring(path.indexOf(prefix) + prefix.length());
String externalRoot = android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
return externalRoot + "/" + path;
}
}
return path;
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage());
return null;
}
}
/**
* 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心)
* 移除 isPng 参数,全程用 PNG 配置
*/
private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
UCrop.Options options = new UCrop.Options();
// 裁剪模式配置(自由裁剪/固定比例)
options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪
// 裁剪配置(优化体验)
//options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
//options.setCompressionQuality(100); // 图片质量
//options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
//options.setToolbarTitle("图片裁剪"); // 工具栏标题
//options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
//options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
// 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键)
options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色
options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色
// 3. 通用 UI 配置(保持原有风格)
options.setHideBottomControls(true); // 隐藏底部控制栏
options.setToolbarTitle("图片裁剪");
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white));
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark));
return options;
}
/**
* 修正文件后缀(强制转为 .png覆盖原有任何图片后缀
*/
private static File correctFileSuffix(File originFile, String targetSuffix) {
String originName = originFile.getName();
// 强制替换所有图片后缀为 targetSuffix避免漏改
originName = originName.replaceAll("\\.(jpg|jpeg|png|bmp|gif)$", "") + "." + targetSuffix;
return new File(originFile.getParent(), originName);
}
/** 生成 FileProvider Uri适配 Android 7.0+ */
private static Uri getFileProviderUri(Activity activity, File file) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
return FileProvider.getUriForFile(activity, authority, file);
} else {
return Uri.fromFile(file);
}
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage());
return null;
}
}
/** 显示 Toast避免崩溃 */
private static void showToast(Activity activity, String msg) {
if (activity != null && !activity.isFinishing()) {
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show();
}
}
// ====================== 公有辅助方法(供外部调用)======================
public static Uri getFileProviderUriPublic(Activity activity, File file) {
return getFileProviderUri(activity, file);
}
public static File getFileFromUriPublic(Activity activity, Uri uri) {
return uriToFile(activity, uri);
}
public static String getPathFromUriPublic(Uri uri) {
return uriToPath(uri);
}
}

View File

@@ -0,0 +1,151 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/22 04:39:40
* @Describe 通知工具类
*/
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.widget.RemoteViews;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import cc.winboll.studio.powerbell.R;
public class NotificationHelper {
public static final String TAG = "NotificationHelper";
// 渠道ID和名称
private static final String CHANNEL_ID_FOREGROUND = "foreground_channel";
private static final String CHANNEL_NAME_FOREGROUND = "Foreground Service";
private static final String CHANNEL_ID_TEMPORARY = "temporary_channel";
private static final String CHANNEL_NAME_TEMPORARY = "Temporary Notifications";
// 通知ID
public static final int FOREGROUND_NOTIFICATION_ID = 1001;
public static final int TEMPORARY_NOTIFICATION_ID = 2001;
private final Context mContext;
private final NotificationManager mNotificationManager;
public NotificationHelper(Context context) {
mContext = context;
mNotificationManager = context.getSystemService(NotificationManager.class);
createNotificationChannels();
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createForegroundChannel();
createTemporaryChannel();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createForegroundChannel() {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_FOREGROUND,
CHANNEL_NAME_FOREGROUND,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Persistent service notifications");
channel.setSound(null, null);
channel.enableVibration(false);
mNotificationManager.createNotificationChannel(channel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createTemporaryChannel() {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_TEMPORARY,
CHANNEL_NAME_TEMPORARY,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Temporary alert notifications");
channel.setSound(null, null);
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
channel.setBypassDnd(true);
mNotificationManager.createNotificationChannel(channel);
}
// 显示常驻通知(通常用于前台服务)
public Notification showForegroundNotification(Intent intent, String title, String content) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_FOREGROUND)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
//.setContentTitle(title + "\n" + content)
.setContentTitle(content)
//.setContentText(content)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build();
mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification);
return notification;
}
// 显示临时通知(自动消失)
public void showTemporaryNotification(Intent intent, String title, String content) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
.setContentTitle(title)
.setContentText(content)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setVibrate(new long[]{100, 200, 300, 400})
.build();
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID, notification);
}
// 创建自定义布局通知(可扩展)
public void showCustomNotification(Intent intent, RemoteViews contentView, RemoteViews bigContentView) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pendingIntent)
.setContent(contentView)
.setCustomBigContentView(bigContentView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build();
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID + 1, notification);
}
// 取消所有通知
public void cancelAllNotifications() {
mNotificationManager.cancelAll();
}
// 创建PendingIntent兼容不同API版本
private PendingIntent createPendingIntent(Intent intent) {
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// flags |= PendingIntent.FLAG_IMMUTABLE;
// }
return PendingIntent.getActivity(
mContext,
0,
intent,
flags
);
}
}

View File

@@ -1,416 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.os.Build;
import android.view.View;
import android.widget.RemoteViews;
import androidx.core.app.NotificationCompat;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/13 20:44
* @Describe 全局通知管理工具类整合所有通知能力适配API29-30兼容Java7所有通知统一跳转MainActivity
*/
public class NotificationManagerUtils {
// ====================== 常量定义(统一管理,避免冲突,首屏可见)======================
public static final String TAG = "NotificationManagerUtils";
// 通知渠道4大渠道场景隔离API26+必填)
// 1. 前台服务保活渠道(低优先级,无打扰)
private static final String CHANNEL_ID_FOREGROUND_SERVICE = "channel_foreground_service";
private static final String CHANNEL_NAME_FOREGROUND_SERVICE = "前台服务保活通知";
private static final String CHANNEL_DESC_FOREGROUND_SERVICE = "后台服务运行状态,无声音无震动,不打扰用户";
// 2. 电量提醒渠道(高优先级,闹钟铃声+震动,强提醒)
private static final String CHANNEL_ID_BATTERY_REMIND = "channel_battery_remind";
private static final String CHANNEL_NAME_BATTERY_REMIND = "电量异常提醒通知";
private static final String CHANNEL_DESC_BATTERY_REMIND = "电量过高/过低提醒,强震动+闹钟铃声,突破免打扰";
// 3. 通用临时通知渠道(高优先级,仅震动,普通告警)
private static final String CHANNEL_ID_TEMP_ALERT = "channel_temp_alert";
private static final String CHANNEL_NAME_TEMP_ALERT = "通用临时提醒通知";
private static final String CHANNEL_DESC_TEMP_ALERT = "普通即时告警,仅震动提醒,自动取消";
// 通知ID唯一区分避免覆盖按场景分段
public static final int NOTIFY_ID_FOREGROUND_SERVICE = 1001; // 前台服务
public static final int NOTIFY_ID_BATTERY_REMIND = 1002; // 电量提醒
public static final int NOTIFY_ID_TEMP_ALERT = 1003; // 通用临时通知
public static final int NOTIFY_ID_CUSTOM_LAYOUT = 1004; // 自定义布局通知
// 通用配置
private static final int PENDING_INTENT_FLAGS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
: PendingIntent.FLAG_UPDATE_CURRENT; // API30安全标志
private static final long[] VIBRATE_PATTERN = new long[]{100, 200, 300, 400}; // 标准震动节奏
//private static final String DEFAULT_JUMP_PACKAGE = "cc.winboll.studio.powerbell"; // 默认跳转包名API29+必填)
// ====================== 成员变量(按场景分组,私有封装,避免外部篡改)======================
private final Context mContext;
private final NotificationManager mNotificationManager;
// 前台服务通知专属
private Notification mForegroundServiceNotify;
private RemoteViews mForegroundServiceRemoteViews;
// 电量提醒通知专属
private Notification mBatteryRemindNotify;
private RemoteViews mBatteryRemindRemoteViews;
// ====================== 构造方法(单例思想/实例化通用,自动初始化渠道)======================
public NotificationManagerUtils(Context context) {
LogUtils.d(TAG, "【初始化】全局通知管理工具类 构造方法调用");
this.mContext = context.getApplicationContext(); // 用应用上下文,避免内存泄漏
this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
createAllNotificationChannels(); // 自动创建所有渠道API26+
LogUtils.d(TAG, "【初始化】全局通知管理工具类 完成,渠道创建状态:" + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "已创建4个渠道" : "无需创建"));
}
// ====================== 核心基础能力(渠道创建+Intent构建复用逻辑减少冗余======================
/**
* 创建所有通知渠道API26+专属,低版本自动跳过,确保通知正常显示)
*/
@SuppressWarnings("deprecation")
public void createAllNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LogUtils.d(TAG, "【渠道管理】开始创建所有通知渠道");
createForegroundServiceChannel();
createBatteryRemindChannel();
createTempAlertChannel();
LogUtils.d(TAG, "【渠道管理】4个通知渠道创建完成含3个核心渠道+预留扩展)");
}
}
/**
* 创建前台服务保活渠道IMPORTANCE_LOW无声音无震动不打扰用户
*/
private void createForegroundServiceChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_FOREGROUND_SERVICE,
CHANNEL_NAME_FOREGROUND_SERVICE,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription(CHANNEL_DESC_FOREGROUND_SERVICE);
channel.setSound(null, null);
channel.enableVibration(false);
channel.setShowBadge(false); // 不显示应用角标
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); // 锁屏隐藏
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】前台服务保活渠道创建成功:" + CHANNEL_NAME_FOREGROUND_SERVICE);
}
}
/**
* 创建电量提醒渠道IMPORTANCE_HIGH闹钟铃声+震动,突破免打扰)
*/
private void createBatteryRemindChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_BATTERY_REMIND,
CHANNEL_NAME_BATTERY_REMIND,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_BATTERY_REMIND);
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(true); // 突破免打扰
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // 锁屏可见
channel.setShowBadge(true);
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】电量提醒渠道创建成功:" + CHANNEL_NAME_BATTERY_REMIND);
}
}
/**
* 创建通用临时通知渠道IMPORTANCE_HIGH仅震动普通告警
*/
private void createTempAlertChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_TEMP_ALERT,
CHANNEL_NAME_TEMP_ALERT,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_TEMP_ALERT);
//channel.setSound(null, null); // 仅震动,不发声
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(false); // 不突破免打扰
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
channel.setShowBadge(true);
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】通用临时通知渠道创建成功:" + CHANNEL_NAME_TEMP_ALERT);
}
}
/**
* 构建固定跳转PendingIntent所有通知统一跳转MainActivity适配API29-30安全规范
* @return 安全的PendingIntent确保跳转稳定不泄露
*/
private PendingIntent buildFixedPendingIntent() {
// 固定跳MainActivity不支持自定义目标
Intent intent = new Intent(mContext, MainActivity.class);
// API29+ 强制要求:明确包名,避免跳转目标模糊
intent.setPackage(mContext.getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); // 跳转时清除栈顶避免重复创建Activity
LogUtils.d(TAG, "【Intent构建】所有通知统一跳转MainActivity包名" + mContext.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(
mContext,
0,
intent,
PENDING_INTENT_FLAGS
);
LogUtils.d(TAG, "【Intent构建】PendingIntent创建成功安全标志" + PENDING_INTENT_FLAGS);
return pendingIntent;
}
// ====================== 场景1前台服务保活通知支持自定义布局+更新)======================
/**
* 初始化前台服务通知自定义布局RemoteViews
*/
private void initForegroundServiceRemoteViews(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【布局初始化】开始初始化前台服务通知布局,标题:" + msg.getTitle());
mForegroundServiceRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView1, msg.getTitle());
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView3, msg.getContent());
mForegroundServiceRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
LogUtils.d(TAG, "【布局初始化】前台服务通知布局填充完成");
}
/**
* 启动前台服务保活通知ControlCenterService专用API26+强制要求,保活后台服务)
*/
public void startForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【前台服务通知】开始构建保活通知,内容:" + msg.getContent());
if (service == null || msg == null) {
LogUtils.e(TAG, "【前台服务通知】构建失败Service/NotificationMessage为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知兼容API26+渠道低版本用Builder
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(service, CHANNEL_ID_FOREGROUND_SERVICE);
} else {
builder = new Notification.Builder(service);
}
mForegroundServiceNotify = builder
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentTitle(msg.getTitle())
.setContentText(msg.getContent())
.setWhen(System.currentTimeMillis())
.setColor(Color.parseColor("#F00606")) // 小图标背景色
.setContentIntent(pendingIntent)
.setOngoing(true) // 常驻通知,不可滑动取消(保活关键)
.setAutoCancel(false) // 禁止点击取消
.build();
// 3. 设置自定义布局
initForegroundServiceRemoteViews(service, msg);
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
// 4. 启动前台服务必须调用否则Service易被回收
service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "【前台服务通知】保活通知启动成功通知ID" + NOTIFY_ID_FOREGROUND_SERVICE);
}
/**
* 更新前台服务保活通知内容(无需重启服务,直接刷新布局)
*/
public void updateForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【前台服务通知】开始更新保活通知,新内容:" + msg.getContent());
if (mForegroundServiceNotify == null || mForegroundServiceRemoteViews == null) {
LogUtils.e(TAG, "【前台服务通知】更新失败通知对象未初始化先调用startForegroundServiceNotify");
return;
}
// 更新自定义布局数据
initForegroundServiceRemoteViews(service, msg);
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
// 发送更新
mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "【前台服务通知】保活通知更新成功");
}
// ====================== 场景2电量提醒通知支持自定义布局+更新+单独取消)======================
/**
* 初始化电量提醒通知自定义布局RemoteViews支持充电/耗电切换)
*/
private void initBatteryRemindRemoteViews(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【布局初始化】开始初始化电量提醒布局,提醒类型:" + msg.getRemindMSG());
mBatteryRemindRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
mBatteryRemindRemoteViews.setTextViewText(R.id.viewremindnotificationTextView1, msg.getTitle());
mBatteryRemindRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
// 切换布局(+:充电提醒,-:耗电提醒)
String remindType = msg.getRemindMSG() != null ? msg.getRemindMSG().trim() : "";
if ("+".equals(remindType)) {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:充电提醒");
} else if ("-".equals(remindType)) {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:耗电提醒");
} else {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
LogUtils.w(TAG, "【布局初始化】未知电量提醒类型:" + remindType);
}
}
/**
* 初始化电量提醒通知仅构建不发送配合update触发提醒
*/
public void initBatteryRemindNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【电量提醒通知】开始初始化提醒通知,标题:" + msg.getTitle());
if (service == null || msg == null) {
LogUtils.e(TAG, "【电量提醒通知】初始化失败Service/NotificationMessage为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(service, CHANNEL_ID_BATTERY_REMIND);
} else {
builder = new Notification.Builder(service);
}
mBatteryRemindNotify = builder
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentTitle(msg.getTitle())
.setContentText(msg.getContent())
.setWhen(System.currentTimeMillis())
.setColor(Color.parseColor("#F00606"))
.setContentIntent(pendingIntent)
.setAutoCancel(true) // 点击取消
.build();
// 3. 初始化自定义布局
initBatteryRemindRemoteViews(service, msg);
mBatteryRemindNotify.contentView = mBatteryRemindRemoteViews;
mBatteryRemindNotify.bigContentView = mBatteryRemindRemoteViews;
LogUtils.d(TAG, "【电量提醒通知】初始化完成");
}
/**
* 发送/更新电量提醒通知(初始化后调用,触发强提醒)
*/
public void sendOrUpdateBatteryRemindNotify() {
LogUtils.d(TAG, "【电量提醒通知】开始发送/更新提醒");
if (mBatteryRemindNotify == null || mBatteryRemindRemoteViews == null) {
LogUtils.e(TAG, "【电量提醒通知】发送失败通知未初始化先调用initBatteryRemindNotify");
return;
}
mNotificationManager.notify(NOTIFY_ID_BATTERY_REMIND, mBatteryRemindNotify);
LogUtils.d(TAG, "【电量提醒通知】发送/更新成功通知ID" + NOTIFY_ID_BATTERY_REMIND);
}
/**
* 单独取消电量提醒通知(静态方法,外部可直接调用,无需实例化)
*/
public static void cancelBatteryRemindNotify(Context context) {
LogUtils.d(TAG, "【电量提醒通知】开始取消提醒通知ID" + NOTIFY_ID_BATTERY_REMIND);
if (context == null) {
LogUtils.e(TAG, "【电量提醒通知】取消失败Context为空");
return;
}
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFY_ID_BATTERY_REMIND);
LogUtils.d(TAG, "【电量提醒通知】取消成功");
}
// ====================== 场景3通用临时通知简单文本自动取消无需自定义布局======================
/**
* 显示通用临时通知普通告警仅震动自动取消统一跳转MainActivity
* @param title 通知标题
* @param content 通知内容
*/
public void showTempAlertNotify(String title, String content) {
LogUtils.d(TAG, "【通用临时通知】开始构建,标题:" + title + ",内容:" + content);
if (title == null || content == null) {
LogUtils.e(TAG, "【通用临时通知】构建失败:标题/内容为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 用NotificationCompat.Builder兼容所有版本简化逻辑
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
.setContentTitle(title)
.setContentText(content)
.setWhen(System.currentTimeMillis())
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setVibrate(VIBRATE_PATTERN);
// 3. 发送通知
Notification notification = builder.build();
mNotificationManager.notify(NOTIFY_ID_TEMP_ALERT, notification);
LogUtils.d(TAG, "【通用临时通知】显示成功通知ID" + NOTIFY_ID_TEMP_ALERT);
}
// ====================== 场景4自定义布局通知灵活扩展支持复杂样式======================
/**
* 显示自定义布局通知(支持普通布局+大布局通用所有场景统一跳转MainActivity
* @param contentView 普通自定义布局(必填)
* @param bigContentView 下拉大布局(可选)
*/
public void showCustomLayoutNotify(RemoteViews contentView, RemoteViews bigContentView) {
LogUtils.d(TAG, "【自定义布局通知】开始构建布局ID" + (contentView != null ? contentView.getLayoutId() : null));
if (contentView == null) {
LogUtils.e(TAG, "【自定义布局通知】构建失败普通布局contentView为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建自定义布局通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
.setSmallIcon(R.drawable.ic_launcher) // 必传,不可省略
.setContentIntent(pendingIntent)
.setContent(contentView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
// 添加大布局(可选)
if (bigContentView != null) {
builder.setCustomBigContentView(bigContentView);
LogUtils.d(TAG, "【自定义布局通知】已添加下拉大布局布局ID" + bigContentView.getLayoutId());
}
// 3. 发送通知
Notification notification = builder.build();
mNotificationManager.notify(NOTIFY_ID_CUSTOM_LAYOUT, notification);
LogUtils.d(TAG, "【自定义布局通知】显示成功通知ID" + NOTIFY_ID_CUSTOM_LAYOUT);
}
// ====================== 通知取消工具(支持精准取消/全取消)======================
/**
* 取消指定ID的通知精准取消灵活控制
*/
public void cancelNotifyById(int notifyId) {
LogUtils.d(TAG, "【通知管理】开始取消通知ID" + notifyId);
mNotificationManager.cancel(notifyId);
LogUtils.d(TAG, "【通知管理】通知取消成功ID" + notifyId);
}
/**
* 取消所有通知(谨慎使用,会清除所有场景的通知)
*/
public void cancelAllNotifies() {
LogUtils.d(TAG, "【通知管理】开始取消所有通知");
mNotificationManager.cancelAll();
LogUtils.d(TAG, "【通知管理】所有通知取消完成");
}
}

View File

@@ -0,0 +1,247 @@
package cc.winboll.studio.powerbell.utils;
/*
* 参考:
* https://blog.csdn.net/qq_35507234/article/details/90676587
* https://blog.csdn.net/qq_16628781/article/details/51548324
*/
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.os.Build;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.RequiresApi;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.beans.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
public class NotificationUtils2 {
public static final String TAG = NotificationHelper.class.getSimpleName();
Context mContext;
NotificationManager mNotificationManager;
Notification mForegroundNotification;
PendingIntent mForegroundPendingIntent;
Notification mRemindNotification;
PendingIntent mRemindPendingIntent;
RemoteViews mrvServiceNotificationView;
RemoteViews mrvRemindNotificationView;
static enum NotificationType { MIN, MAX };
private static int _mnServiceNotificationID = 1;
private static int _mnRemindNotificationID = 2;
private static String _mszChannelIDService = "1";
private static String _mszChannelNameService = "Service";
private static String _mszChannelIDRemind = "2";
private static String _mszChannelNameRemind = "Remind";
// public NotificationUtils(Context context) {
// mContext = context;
// mNotificationManager = (NotificationManager) context.getSystemService(
// Context.NOTIFICATION_SERVICE);
// }
public NotificationUtils2(Context context) {
mContext = context;
mNotificationManager = context.getSystemService(NotificationManager.class);
//createNotificationChannels();
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createServiceChannel();
createRemindChannel();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createServiceChannel() {
NotificationChannel channel = new NotificationChannel(
_mszChannelIDService,
_mszChannelNameService,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Background service updates");
channel.setSound(null, null);
channel.enableVibration(false);
mNotificationManager.createNotificationChannel(channel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createRemindChannel() {
NotificationChannel channel = new NotificationChannel(
_mszChannelIDRemind,
_mszChannelNameRemind,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Critical reminders");
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM), null);
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
channel.setBypassDnd(true);
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
mNotificationManager.createNotificationChannel(channel);
}
// 创建并发送服务通知
//
public void createForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//创建Notification传入Context和channelId
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
intent.setPackage(service.getPackageName());
//LogUtils.d(TAG, "mService.getPackageName() : " + service.getPackageName());
intent.setClass(service, MainActivity.class);
//LogUtils.d(TAG, "MainActivity.class.getName() : " + MainActivity.class.getName());
//这里放一个count用来区分每一个通知
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
//参数1:context 上下文对象
//参数2:发送者私有的请求码(Private request code for the sender)
//参数3:intent 意图对象
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
//mForegroundPendingIntent = PendingIntent.getActivity(mService, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mForegroundPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mForegroundPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_IMMUTABLE);
}
mForegroundNotification = new Notification.Builder(service, _mszChannelIDService)
.setAutoCancel(true)
.setContentTitle(notificationMessage.getTitle())
.setContentText(notificationMessage.getContent())
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_launcher)
//设置红色
.setColor(Color.parseColor("#F00606"))
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentIntent(mForegroundPendingIntent)
.build();
setForegroundNotificationRemoteViews(service, notificationMessage);
service.startForeground(_mnServiceNotificationID, mForegroundNotification);
}
void initmrvRemindNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
mrvRemindNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
mrvRemindNotificationView.setTextViewText(R.id.viewremindnotificationTextView1, notificationMessage.getTitle());
String szRemindMSG = notificationMessage.getRemindMSG();
//LogUtils.d(TAG, "szRemindMSG : " + szRemindMSG);
//mrvRemindNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
if (szRemindMSG != null) {
if (szRemindMSG.trim().equals("-")) {
//LogUtils.d(TAG, "-");
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
} else if (szRemindMSG.trim().equals("+")) {
//LogUtils.d(TAG, "+");
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.GONE);
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
}
mrvRemindNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
//给我remoteViews上的控件tv_content添加监听事件
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
//return mrvServiceNotificationView;
}
}
void initmrvServiceNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
mrvServiceNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView1, notificationMessage.getTitle());
//String szRemindMSG = notificationMessage.getRemindMSG();
//mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
//rvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent() + Integer.toString(nTest));
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent());
mrvServiceNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
//给我remoteViews上的控件tv_content添加监听事件
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
//return mrvServiceNotificationView;
}
void setForegroundNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
initmrvServiceNotificationView(service, notificationMessage);
mForegroundNotification.contentView = mrvServiceNotificationView;
mForegroundNotification.bigContentView = mrvServiceNotificationView;
}
void setRemindNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
initmrvRemindNotificationView(service, notificationMessage);
mRemindNotification.contentView = mrvRemindNotificationView;
mRemindNotification.bigContentView = mrvRemindNotificationView;
}
// 更新服务通知
//
public void updateForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
setForegroundNotificationRemoteViews(service, notificationMessage);
mNotificationManager.notify(_mnServiceNotificationID, mForegroundNotification);
}
// 创建并发送电量提醒通知
//
public void updateRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//LogUtils.d(TAG, "updateRemindNotification : " + notificationMessage.getRemindMSG());
setRemindNotificationRemoteViews(service, notificationMessage);
mNotificationManager.notify(_mnRemindNotificationID, mRemindNotification);
}
public void createRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//LogUtils.d(TAG, "notificationMessage : " + notificationMessage.getRemindMSG());
//创建Notification传入Context和channelId
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
intent.setPackage(service.getPackageName());
intent.setClass(service, MainActivity.class);
//这里放一个count用来区分每一个通知
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
//参数1:context 上下文对象
//参数2:发送者私有的请求码(Private request code for the sender)
//参数3:intent 意图对象
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
//mRemindPendingIntent = PendingIntent.getActivity(mService, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mRemindPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mRemindPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_IMMUTABLE);
}
mRemindNotification = new Notification.Builder(service, _mszChannelIDRemind)
.setAutoCancel(true)
.setContentTitle(notificationMessage.getTitle())
.setContentText(notificationMessage.getContent())
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_launcher)
//设置红色
.setColor(Color.parseColor("#F00606"))
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentIntent(mRemindPendingIntent)
.build();
setRemindNotificationRemoteViews(service, notificationMessage);
}
public static void cancelRemindNotification(Context context){
// 获取 NotificationManager 实例
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 撤回指定 ID 的通知栏消息
notificationManager.cancel(_mnRemindNotificationID);
}
}

View File

@@ -1,350 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/14 03:05
* @Describe 权限申请工具类Java7兼容版
* 适配 小米手机+API29-30整合自启动、电池优化、全文件管理权限专注后台保活核心权限
*/
public class PermissionUtils {
// ====================== 常量定义(首屏可见,统一管理,避免冲突)======================
// 日志标签
public static final String TAG = "PermissionUtils";
// 权限请求码(按场景分段,避免重复)
public static final int REQUEST_IGNORE_BATTERY_OPTIMIZATION = 1000; // 电池优化权限
public static final int REQUEST_AUTO_START = 1001; // 自启动权限(小米专属)
public static final int REQUEST_ALL_FILE_MANAGE = 1002; // 全文件管理权限API30+
// SDK版本常量适配API29-30替代系统枚举Java7兼容
private static final int SDK_VERSION_Q = 29; // Android 10API29
private static final int SDK_VERSION_R = 30; // Android 11API30
// 小米自启动权限页面配置(专属跳转路径,精准适配)
private static final String XIAOMI_AUTO_START_PACKAGE = "com.miui.securitycenter";
private static final String XIAOMI_AUTO_START_CLASS = "com.miui.permcenter.autostart.AutoStartManagementActivity";
// ====================== 单例模式Java7标准双重校验锁线程安全+懒加载)======================
private static volatile PermissionUtils sInstance;
private PermissionUtils() {}
public static PermissionUtils getInstance() {
if (sInstance == null) {
synchronized (PermissionUtils.class) {
if (sInstance == null) {
sInstance = new PermissionUtils();
LogUtils.d(TAG, "初始化PermissionUtils 单例创建成功");
}
}
}
return sInstance;
}
// ====================== 核心权限1全文件管理权限API29-30适配通用所有机型======================
/**
* 检查全文件管理权限适配API30+ MANAGE_EXTERNAL_STORAGE兼容API29-旧权限)
* @param activity 上下文Activity不可为null
* @return true=权限已授予false=权限未授予
*/
public boolean checkAllFileManagePermission(Activity activity) {
LogUtils.d(TAG, "全文件权限-检查:开始校验,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null) {
LogUtils.e(TAG, "全文件权限-检查失败Activity为空");
return false;
}
// API30+:校验 MANAGE_EXTERNAL_STORAGE 特殊权限
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
boolean hasManagePerm = Environment.isExternalStorageManager();
LogUtils.d(TAG, "全文件权限-检查API30+MANAGE_EXTERNAL_STORAGE权限=" + (hasManagePerm ? "已授予" : "未授予"));
return hasManagePerm;
} else if (Build.VERSION.SDK_INT == SDK_VERSION_Q) {
LogUtils.d(TAG, "全文件权限-检查API29无需申请默认支持文件管理");
return true;
} else {
boolean hasWritePerm = ContextCompat.checkSelfPermission(activity,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
LogUtils.d(TAG, "全文件权限-检查API29以下WRITE_EXTERNAL_STORAGE权限=" + (hasWritePerm ? "已授予" : "未授予"));
return hasWritePerm;
}
}
/**
* 申请全文件管理权限适配API30+特殊权限流程兼容API29-旧权限申请)
* @param activity 申请权限的Activity不可为null
*/
public void requestAllFileManagePermission(Activity activity) {
LogUtils.d(TAG, "全文件权限-申请:开始处理,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "全文件权限-申请失败Activity无效/已销毁");
return;
}
// 先检查权限,已授予直接返回
if (checkAllFileManagePermission(activity)) {
LogUtils.d(TAG, "全文件权限-申请:已拥有权限,无需发起");
return;
}
// API30+:跳转系统特殊权限申请页(用户手动授权)
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
LogUtils.d(TAG, "全文件权限-申请API30+,跳转特殊权限申请页");
} catch (Exception e) {
// 备用跳转:系统设置首页,引导手动操作
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
LogUtils.w(TAG, "全文件权限-申请:跳转失败,引导手动开启");
showAllFileManageTipsDialog(activity);
}
} else {
ActivityCompat.requestPermissions(activity,
new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_ALL_FILE_MANAGE);
LogUtils.d(TAG, "全文件权限-申请API29以下发起WRITE_EXTERNAL_STORAGE权限申请");
}
}
// ====================== 核心权限2自启动权限小米专属API29-30适配======================
/**
* 检查自启动权限(仅小米机型需要,非小米直接返回无需申请)
* @param activity 上下文Activity不可为null
* @return true=小米机型需手动开启false=非小米机型(无需申请)
*/
// public boolean checkAutoStartPermission(Activity activity) {
// LogUtils.d(TAG, "自启动权限-检查:开始,设备品牌=" + Build.BRAND);
// if (activity == null) {
// LogUtils.e(TAG, "自启动权限-检查失败Activity为空");
// return false;
// }
//
// boolean isXiaomi = Build.BRAND.toLowerCase().contains("xiaomi");
// LogUtils.d(TAG, "自启动权限-检查:结果=" + (isXiaomi ? "小米机型(需开启)" : "非小米机型(无需申请)"));
// return isXiaomi;
// }
/**
* 请求自启动权限小米专属多方案跳转适配API29-30机型差异
* @param activity 申请权限的Activity不可为null
*/
public void requestAutoStartPermission(Activity activity) {
LogUtils.d(TAG, "自启动权限-申请:开始处理");
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "自启动权限-申请失败Activity无效/已销毁");
return;
}
// 非小米机型,直接返回
// if (!checkAutoStartPermission(activity)) {
// LogUtils.d(TAG, "自启动权限-申请:非小米机型,无需处理");
// return;
// }
// API30+ 小米:优先精准跳转自启动管理页
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
try {
// 方案1组件名精准跳转成功率最高
Intent intent = new Intent();
intent.setComponent(new ComponentName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS));
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-申请API30+,组件名跳转自启动管理页");
} catch (Exception e1) {
try {
// 方案2Action备用跳转兼容机型差异
Intent intent = new Intent("miui.intent.action.OP_AUTO_START");
intent.setClassName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-申请API30+Action跳转自启动管理页");
} catch (Exception e2) {
// 方案3终极备用跳转系统设置+提示
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.w(TAG, "自启动权限-申请:跳转失败,引导手动操作");
showAutoStartTipsDialog(activity);
}
}
return;
}
// API29 小米:低版本兼容跳转
try {
Intent intent = new Intent(XIAOMI_AUTO_START_CLASS);
intent.setPackage(XIAOMI_AUTO_START_PACKAGE);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-申请API29低版本跳转自启动管理页");
} catch (Exception e) {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
showAutoStartTipsDialog(activity);
}
}
// ====================== 核心权限3电池优化权限通用所有机型API29-30适配======================
/**
* 检查忽略电池优化权限精准判断API23+有效,低版本视为已拥有)
* @param activity 上下文Activity不可为null
* @return true=已忽略优化false=未忽略(需申请)
*/
public boolean checkIgnoreBatteryOptimizationPermission(Activity activity) {
LogUtils.d(TAG, "电池优化权限-检查:开始,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null) {
LogUtils.e(TAG, "电池优化权限-检查失败Activity为空");
return false;
}
// API23以下无此权限视为已拥有
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
LogUtils.d(TAG, "电池优化权限-检查API23以下无需校验视为已拥有");
return true;
}
// API23+ 精准校验权限状态
PowerManager powerManager = (PowerManager) activity.getSystemService(Activity.POWER_SERVICE);
if (powerManager == null) {
LogUtils.e(TAG, "电池优化权限-检查获取PowerManager失败校验异常");
return false;
}
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());
LogUtils.d(TAG, "电池优化权限-检查:结果=" + (isIgnored ? "已忽略优化" : "未忽略(需申请)"));
return isIgnored;
}
/**
* 请求忽略电池优化权限多方案跳转适配API29-30自动判断是否需要申请
* @param activity 申请权限的Activity不可为null
*/
public void requestIgnoreBatteryOptimizationPermission(Activity activity) {
LogUtils.d(TAG, "电池优化权限-申请:开始处理");
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "电池优化权限-申请失败Activity无效/已销毁");
return;
}
// 已拥有权限,直接返回
if (checkIgnoreBatteryOptimizationPermission(activity)) {
LogUtils.d(TAG, "电池优化权限-申请:已拥有权限,无需发起");
return;
}
try {
// 方案1直接跳转一键授权页优先使用用户操作简单
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.d(TAG, "电池优化权限-申请:跳转一键授权页");
} catch (Exception e) {
// 方案2备用跳转优化管理页+提示
Intent intent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS);
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.w(TAG, "电池优化权限-申请:跳转失败,引导手动操作");
showBatteryOptTipsDialog(activity);
}
}
// ====================== 辅助方法:手动开启提示弹窗(适配跳转失败场景)======================
/**
* 全文件管理权限手动开启提示弹窗
*/
private void showAllFileManageTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("全文件管理权限申请提示")
.setMessage("请手动开启全文件管理权限,否则文件操作功能异常:\n1. 进入设置 → 应用 → 本应用 → 权限\n2. 找到「文件管理」/「存储」权限,开启「允许管理所有文件」")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
LogUtils.d(TAG, "全文件权限:显示手动开启提示弹窗");
}
/**
* 自启动权限手动开启提示弹窗(小米专属)
*/
private void showAutoStartTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("自启动权限申请提示")
.setMessage("请手动开启自启动权限,否则应用后台保活异常:\n1. 进入小米安全中心 → 应用管理 → 自启动管理\n2. 找到本应用,开启「允许自启动」开关")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
LogUtils.d(TAG, "自启动权限:显示手动开启提示弹窗");
}
/**
* 电池优化权限手动开启提示弹窗
*/
private void showBatteryOptTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("电池优化权限申请提示")
.setMessage("请手动忽略电池优化,否则应用后台运行被限制:\n1. 进入设置 → 电池 → 电池优化\n2. 找到本应用,选择「不优化」选项")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
LogUtils.d(TAG, "电池优化权限:显示手动开启提示弹窗");
}
public void startPermissionRequest(final Activity activity) {
// 电池优化权限(通用所有机型)
if (!checkIgnoreBatteryOptimizationPermission(activity)) {
YesNoAlertDialog.show(activity, activity.getString(R.string.app_name) + "权限申请提示:", "本应用要正常使用,需要申请电池优化与自启动权限。是否进入权限设置步骤?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onNo() {
ToastUtils.show(activity.getString(R.string.app_name) + "应用可能无法正常使用。");
}
@Override
public void onYes() {
requestIgnoreBatteryOptimizationPermission(activity);
}
});
}
}
public void handlePermissionRequest(final Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == PermissionUtils.REQUEST_IGNORE_BATTERY_OPTIMIZATION) {
// 自启动权限(小米专属)
// 小米机型,发起自启动权限申请
requestAutoStartPermission(activity);
} else if (requestCode == PermissionUtils.REQUEST_AUTO_START) {
// 自启动权限(小米专属)
if (App.isDebugging() && !checkAllFileManagePermission(activity)) {
// 小米机型,发起自启动权限申请
requestAllFileManagePermission(activity);
}
}
}
}

View File

@@ -0,0 +1,207 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/21 18:55
* @Describe
* 图片下载工具类指定目录保存Pictures/PowerBell/BackgroundHistory
*/
public class PictureUtils {
private static final String TAG = "PictureUtils";
private static final String ROOT_DIR = "PowerBell/BackgroundHistory"; // 自定义目录结构
private static OkHttpClient sOkHttpClient;
static {
sOkHttpClient = new OkHttpClient();
}
/**
* 下载网络图片到指定目录(外部存储/Pictures/PowerBell/BackgroundHistory
* @param context 上下文(用于通知相册刷新)
* @param imgUrl 图片网络URL
* @param callback 下载结果回调(成功/失败)
*/
public static void downloadImageToAlbum(final Context context, final String imgUrl, final DownloadCallback callback) {
// 检查参数合法性
if (context == null) {
if (callback != null) {
callback.onFailure(new IllegalArgumentException("Context不能为空"));
}
return;
}
if (imgUrl == null || imgUrl.isEmpty()) {
if (callback != null) {
callback.onFailure(new IllegalArgumentException("图片URL为空"));
}
return;
}
startDownload(context, imgUrl, callback);
}
/**
* 执行实际的下载逻辑
*/
private static void startDownload(final Context context, final String imgUrl, final DownloadCallback callback) {
Request request = new Request.Builder().url(imgUrl).build();
sOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
if (callback != null) {
callback.onFailure(new IOException("请求失败,响应码:" + response.code()));
}
return;
}
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = response.body().byteStream();
// 1. 获取并创建指定保存目录(外部存储/Pictures/PowerBell/BackgroundHistory
File saveDir = getTargetSaveDir(context);
if (!saveDir.exists()) {
boolean isDirCreated = saveDir.mkdirs(); // 递归创建多级目录
if (!isDirCreated) {
if (callback != null) {
callback.onFailure(new IOException("创建目录失败:" + saveDir.getAbsolutePath()));
}
return;
}
}
// 2. 解析图片后缀
String fileSuffix = getImageSuffix(imgUrl, response);
// 3. 生成时间戳文件名
String fileName = generateTimeFileName() + fileSuffix;
// 4. 创建文件
final File saveFile = new File(saveDir, fileName);
// 5. 写入文件
outputStream = new FileOutputStream(saveFile);
byte[] buffer = new byte[1024 * 4];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
outputStream.flush();
// 6. 通知相册刷新(使图片显示在系统相册中)
notifyAlbumRefresh(context, saveFile);
// 成功回调
if (callback != null) {
callback.onSuccess(saveFile.getAbsolutePath());
}
} catch (Exception e) {
Log.e(TAG, "下载图片异常", e);
if (callback != null) {
callback.onFailure(e);
}
} finally {
// 关闭资源
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
if (response.body() != null) response.body().close();
}
}
@Override
public void onFailure(Call call, final IOException e) {
Log.e(TAG, "下载图片失败", e);
if (callback != null) {
callback.onFailure(e);
}
}
});
}
/**
* 获取目标保存目录:外部存储/Pictures/PowerBell/BackgroundHistory
*/
private static File getTargetSaveDir(Context context) {
// 优先使用公共Pictures目录兼容多数设备
File publicPicturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
if (publicPicturesDir.exists()) {
return new File(publicPicturesDir, ROOT_DIR);
}
// 备选应用私有Pictures目录若公共目录不可用
File appPicturesDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (appPicturesDir != null) {
return new File(appPicturesDir, ROOT_DIR);
}
// 极端情况:外部存储根目录
return new File(Environment.getExternalStorageDirectory(), ROOT_DIR);
}
/**
* 解析图片后缀名
*/
private static String getImageSuffix(String imgUrl, Response response) {
// 优先从URL解析
if (imgUrl.lastIndexOf('.') != -1) {
String suffix = imgUrl.substring(imgUrl.lastIndexOf('.'));
if (suffix.length() <= 5 && (suffix.contains("png") || suffix.contains("jpg") || suffix.contains("jpeg") || suffix.contains("gif"))) {
return suffix.toLowerCase(Locale.getDefault());
}
}
// 从响应头解析
String contentType = response.header("Content-Type");
if (contentType != null) {
if (contentType.contains("png")) return ".png";
if (contentType.contains("jpeg") || contentType.contains("jpg")) return ".jpg";
if (contentType.contains("gif")) return ".gif";
}
return ".jpg"; // 默认后缀
}
/**
* 生成时间戳文件名
*/
private static String generateTimeFileName() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault());
return sdf.format(new Date());
}
/**
* 通知相册刷新
*/
private static void notifyAlbumRefresh(Context context, File file) {
try {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri uri = Uri.fromFile(file);
intent.setData(uri);
context.sendBroadcast(intent);
} catch (Exception e) {
Log.e(TAG, "通知相册刷新失败", e);
}
}
/**
* 下载结果回调接口
*/
public interface DownloadCallback {
void onSuccess(String savePath); // 下载成功(子线程回调)
void onFailure(Exception e); // 下载失败(子线程回调)
}
}

View File

@@ -1,6 +1,6 @@
package cc.winboll.studio.powerbell.utils;
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import cc.winboll.studio.powerbell.beans.BatteryInfoBean;
import java.util.ArrayList;
public class StringUtils {

View File

@@ -0,0 +1,145 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/28 04:23:04
* @Describe UriUtil
*/
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class UriUtil {
public static final String TAG = "UriUtil";
/**
* 获取真实路径
*
* @param context
*/
public static String getFilePathFromUri(Context context, Uri uri) {
if (uri == null) {
return null;
}
switch (uri.getScheme()) {
case ContentResolver.SCHEME_CONTENT:
//Android7.0之后的uri content:// URI
return getFilePathFromContentUri(context, uri);
case ContentResolver.SCHEME_FILE:
default:
//Android7.0之前的uri file://
return new File(uri.getPath()).getAbsolutePath();
}
}
/**
* 从uri获取path
*
* @param uri content://media/external/file/109009
* <p>
* FileProvider适配
* content://com.tencent.mobileqq.fileprovider/external_files/storage/emulated/0/Tencent/QQfile_recv/
* content://com.tencent.mm.external.fileprovider/external/tencent/MicroMsg/Download/
*/
private static String getFilePathFromContentUri(Context context, Uri uri) {
if (null == uri) return null;
String data = null;
String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (index > -1) {
data = cursor.getString(index);
} else {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
String fileName = cursor.getString(nameIndex);
data = getPathFromInputStreamUri(context, uri, fileName);
}
}
cursor.close();
}
return data;
}
/**
* 用流拷贝文件一份到自己APP私有目录下
*
* @param context
* @param uri
* @param fileName
*/
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
InputStream inputStream = null;
String filePath = null;
if (uri.getAuthority() != null) {
try {
inputStream = context.getContentResolver().openInputStream(uri);
File file = createTemporalFileFrom(context, inputStream, fileName);
filePath = file.getPath();
} catch (Exception e) {
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
}
}
}
return filePath;
}
public static Uri getUriForFile(Context context, File file) {
//Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
}
return Uri.fromFile(file);
}
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)
throws IOException {
File targetFile = null;
if (inputStream != null) {
int read;
byte[] buffer = new byte[8 * 1024];
//自己定义拷贝文件路径
targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) {
targetFile.delete();
}
OutputStream outputStream = new FileOutputStream(targetFile);
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
outputStream.flush();
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return targetFile;
}
}

View File

@@ -1,481 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* Uri 工具类Java7兼容适配API29-30+小米机型FileProvider安全适配
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/28
*/
public class UriUtils {
// ====================== 常量定义(顶部统一管理)======================
public static final String TAG = "UriUtils";
// FileProvider 授权后缀与Manifest配置保持一致
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// 应用公共图片目录API29+ 适配替代废弃API
private static final String APP_PUBLIC_PIC_DIR = "PowerBell/";
// MIME类型与文件后缀映射表覆盖常见格式小米机型精准匹配
private static final Map<String, String> MIME_SUFFIX_MAP = new HashMap<String, String>() {{
// 图片格式(重点,含透明格式)
put("image/png", "png");
put("image/jpeg", "jpg");
put("image/jpg", "jpg");
put("image/gif", "gif");
put("image/bmp", "bmp");
put("image/webp", "webp");
// 音视频格式
put("video/mp4", "mp4");
put("video/avi", "avi");
put("video/mkv", "mkv");
put("audio/mp3", "mp3");
put("audio/wav", "wav");
// 文档格式
put("application/pdf", "pdf");
put("application/msword", "doc");
put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
put("application/vnd.ms-excel", "xls");
put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
}};
// ====================== 新增核心方法Uri 转文件后缀 ======================
/**
* 【静态公共方法】根据 Uri 获取文件真实后缀优先MIME类型匹配适配所有Uri场景+小米机型)
* @param context 上下文非空用于获取ContentResolver
* @param uri 待解析 Uri支持 content:// / file:// 双Scheme
* @return 小写文件后缀(如 png/jpg/mp4无匹配返回空字符串
*/
public static String getSuffixFromUri(Context context, Uri uri) {
LogUtils.d(TAG, "=== getSuffixFromUri 调用 startUri" + (uri != null ? uri.toString() : "null") + " ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getSuffixFromUriContext 为空,获取失败");
return "";
}
if (uri == null) {
LogUtils.e(TAG, "getSuffixFromUriUri 为空,获取失败");
return "";
}
String suffix = "";
String scheme = uri.getScheme();
// 2. 按 Uri Scheme 分类处理(优先精准匹配,再降级截取)
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
// 场景1content:// Uri优先通过MIME类型获取最精准
suffix = getSuffixFromContentUri(context, uri);
LogUtils.d(TAG, "getSuffixFromUricontent:// UriMIME匹配后缀" + suffix);
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
// 场景2file:// Uri直接解析文件名截取后缀
String filePath = new File(uri.getPath()).getAbsolutePath();
suffix = getSuffixFromFilePath(filePath);
LogUtils.d(TAG, "getSuffixFromUrifile:// Uri路径截取后缀" + suffix);
} else {
// 场景3未知Scheme尝试解析Uri路径截取兜底
String uriPath = uri.getPath();
suffix = uriPath != null ? getSuffixFromFilePath(uriPath) : "";
LogUtils.w(TAG, "getSuffixFromUri未知Scheme=" + scheme + ",兜底截取后缀:" + suffix);
}
// 3. 最终结果处理(统一小写,去空)
suffix = suffix != null ? suffix.trim().toLowerCase() : "";
LogUtils.d(TAG, "=== getSuffixFromUri 调用 end最终后缀" + suffix + " ===");
return suffix;
}
// ====================== 公有核心方法(对外提供能力,按功能排序)======================
/**
* Uri 转真实文件路径(核心方法,适配 content:// / file:// 双 Scheme
* @param context 上下文(非空)
* @param uri 待转换 Uri非空
* @return 真实文件绝对路径(转换失败返回 null
*/
public static String getFilePathFromUri(Context context, Uri uri) {
LogUtils.d(TAG, "=== getFilePathFromUri 调用 start ===");
if (context == null) {
LogUtils.e(TAG, "getFilePathFromUriContext 为空,转换失败");
return null;
}
if (uri == null) {
LogUtils.e(TAG, "getFilePathFromUriUri 为空,转换失败");
return null;
}
String scheme = uri.getScheme();
String filePath = null;
// 按 Uri Scheme 分类处理
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
LogUtils.d(TAG, "getFilePathFromUriScheme=content执行ContentUri转换");
filePath = getFilePathFromContentUri(context, uri);
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
LogUtils.d(TAG, "getFilePathFromUriScheme=file直接转换路径");
filePath = new File(uri.getPath()).getAbsolutePath();
} else {
LogUtils.w(TAG, "getFilePathFromUri未知Scheme=" + scheme + ",转换失败");
}
LogUtils.d(TAG, "=== getFilePathFromUri 调用 end结果" + filePath + " ===");
return filePath;
}
/**
* 文件路径转 Uri核心方法适配 Android7.0+ FileProviderAPI29-30兼容
* @param context 上下文(非空)
* @param filePath 真实文件路径(非空)
* @return 安全 Uri转换失败返回 null
*/
public static Uri getUriForFile(Context context, String filePath) {
LogUtils.d(TAG, "=== getUriForFile路径版调用 start ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getUriForFileContext 为空,转换失败");
return null;
}
if (filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "getUriForFile文件路径为空转换失败");
return null;
}
// 2. File 对象初始化与校验
File file = new File(filePath);
LogUtils.d(TAG, "getUriForFile文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile文件不存在或为目录转换失败");
return null;
}
// 3. 合法路径校验适配小米机型避免FileProvider配置外路径
if (!isPathInValidDir(context, file)) {
LogUtils.w(TAG, "getUriForFile路径不在安全配置目录内小米机型可能出现权限异常");
}
// 4. 调用重载方法生成 Uri
Uri uri = getUriForFile(context, file);
LogUtils.d(TAG, "=== getUriForFile路径版调用 end结果" + (uri != null ? uri.toString() : "null") + " ===");
return uri;
}
/**
* File 对象转 Uri重载方法直接接收File内部安全适配
* @param context 上下文(非空)
* @param file 待转换 File 对象(非空)
* @return 安全 Uri转换失败返回 null
*/
public static Uri getUriForFile(Context context, File file) {
LogUtils.d(TAG, "=== getUriForFileFile版调用 start ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getUriForFileContext 为空,转换失败");
return null;
}
if (file == null) {
LogUtils.e(TAG, "getUriForFileFile 对象为空,转换失败");
return null;
}
LogUtils.d(TAG, "getUriForFile文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile文件不存在或为目录转换失败");
return null;
}
// 2. 按系统版本生成 UriAPI24+ 强制 FileProvider适配小米机型
Uri uri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LogUtils.d(TAG, "getUriForFileAndroid7.0+使用FileProvider生成Uri");
String authority = context.getPackageName() + FILE_PROVIDER_SUFFIX;
LogUtils.d(TAG, "getUriForFileFileProvider Authority=" + authority);
try {
uri = FileProvider.getUriForFile(context, authority, file);
LogUtils.d(TAG, "getUriForFileContent Uri生成成功=" + uri.toString());
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "getUriForFileFileProvider生成失败小米机型常见原因路径未配置/Authority不匹配", e);
}
} else {
LogUtils.d(TAG, "getUriForFileAndroid7.0以下使用Uri.fromFile生成");
uri = Uri.fromFile(file);
LogUtils.d(TAG, "getUriForFileFile Uri生成成功=" + uri.toString());
}
LogUtils.d(TAG, "=== getUriForFileFile版调用 end ===");
return uri;
}
// ====================== 私有辅助方法(内部逻辑封装,不对外暴露)======================
/**
* ContentUri 转真实路径(适配 content:// 格式处理小米机型特殊Uri
* @param context 上下文
* @param uri ContentUricontent://media/external/file/xxx
* @return 真实文件路径(失败返回 null
*/
private static String getFilePathFromContentUri(Context context, Uri uri) {
LogUtils.d(TAG, "getFilePathFromContentUriUri=" + uri.toString());
String filePath = null;
Cursor cursor = null;
// Java7 语法try-catch-finally 手动关闭Cursor避免内存泄漏
try {
// 查询字段:优先 DATA 字段,失败则通过文件名+流拷贝获取
String[] queryColumns = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
// 优先读取 DATA 字段(直接获取路径)
int dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (dataIndex != -1) {
filePath = cursor.getString(dataIndex);
LogUtils.d(TAG, "getFilePathFromContentUri从DATA字段获取路径=" + filePath);
} else {
// DATA 字段为空,通过流拷贝到私有目录获取路径(小米机型特殊场景适配)
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
String fileName = cursor.getString(nameIndex);
LogUtils.d(TAG, "getFilePathFromContentUriDATA字段为空通过流拷贝获取文件名=" + fileName);
filePath = getPathFromInputStreamUri(context, uri, fileName);
}
}
} catch (Exception e) {
LogUtils.e(TAG, "getFilePathFromContentUri查询失败", e);
} finally {
// 强制关闭Cursor避免资源泄漏Java7 必须手动处理)
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
LogUtils.e(TAG, "getFilePathFromContentUri关闭Cursor失败", e);
}
}
}
return filePath;
}
/**
* 流拷贝获取路径(适配无 DATA 字段的 ContentUri小米机型特殊Uri兼容
* 将目标文件拷贝到应用私有缓存目录,返回拷贝后的路径
* @param context 上下文
* @param uri ContentUri
* @param fileName 文件名
* @return 拷贝后的文件路径(失败返回 null
*/
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
LogUtils.d(TAG, "getPathFromInputStreamUri开始流拷贝文件名=" + fileName);
InputStream inputStream = null;
OutputStream outputStream = null;
File targetFile = null;
try {
// 1. 打开输入流读取Uri对应文件
inputStream = context.getContentResolver().openInputStream(uri);
if (inputStream == null) {
LogUtils.e(TAG, "getPathFromInputStreamUri输入流打开失败");
return null;
}
// 2. 创建目标文件(应用私有缓存目录,无权限限制)
targetFile = new File(context.getExternalCacheDir(), fileName);
// 若文件已存在,先删除(避免覆盖导致格式异常)
if (targetFile.exists()) {
boolean deleteSuccess = targetFile.delete();
LogUtils.d(TAG, "getPathFromInputStreamUri删除已存在文件结果=" + deleteSuccess);
}
// 3. 流拷贝Java7 手动处理流,避免 try-with-resources
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[8 * 1024]; // 8KB 缓冲区,平衡效率与内存
int readLength;
while ((readLength = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, readLength);
}
outputStream.flush();
LogUtils.d(TAG, "getPathFromInputStreamUri流拷贝成功路径=" + targetFile.getAbsolutePath());
} catch (Exception e) {
LogUtils.e(TAG, "getPathFromInputStreamUri流拷贝失败", e);
// 拷贝失败,删除临时文件
if (targetFile != null && targetFile.exists()) {
targetFile.delete();
}
targetFile = null;
} finally {
// 强制关闭流避免资源泄漏Java7 必须手动关闭)
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
LogUtils.e(TAG, "getPathFromInputStreamUri关闭输出流失败", e);
}
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
LogUtils.e(TAG, "getPathFromInputStreamUri关闭输入流失败", e);
}
}
return targetFile != null ? targetFile.getAbsolutePath() : null;
}
/**
* 校验路径是否在安全目录内适配API29-30+小米机型避免FileProvider权限异常
* 仅允许:应用私有目录、缓存目录、应用专属公共目录
* @param context 上下文
* @param file 待校验文件
* @return true=安全路径false=非安全路径
*/
private static boolean isPathInValidDir(Context context, File file) {
String absolutePath = file.getAbsolutePath();
// 1. 应用外部私有目录API29+ 推荐,无权限限制)
String externalPrivateDir = context.getExternalFilesDir(null) != null
? context.getExternalFilesDir(null).getAbsolutePath()
: "";
// 2. 应用内部私有目录(无权限限制)
String internalPrivateDir = context.getFilesDir().getAbsolutePath();
// 3. 应用缓存目录(无权限限制)
String cacheDir = context.getCacheDir().getAbsolutePath();
// 4. 应用专属公共目录API29+ 适配,替代废弃的 getExternalStoragePublicDirectory
String appPublicDir = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator + Environment.DIRECTORY_PICTURES
+ File.separator + APP_PUBLIC_PIC_DIR;
// 校验路径是否在安全目录内小米机型必须严格校验否则FileProvider会抛异常
boolean isInValidDir = absolutePath.startsWith(externalPrivateDir)
|| absolutePath.startsWith(internalPrivateDir)
|| absolutePath.startsWith(cacheDir)
|| absolutePath.startsWith(appPublicDir);
LogUtils.d(TAG, "isPathInValidDir外部私有目录=" + externalPrivateDir
+ ",公共目录=" + appPublicDir
+ ",校验结果=" + isInValidDir);
return isInValidDir;
}
/**
* 流拷贝创建临时文件(内部辅助,封装拷贝逻辑)
* @param context 上下文
* @param inputStream 输入流
* @param fileName 文件名
* @return 临时文件(失败返回 null
* @throws IOException 流操作异常
*/
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) throws IOException {
File targetFile = null;
if (inputStream != null) {
byte[] buffer = new byte[8 * 1024];
int readLength;
targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) {
targetFile.delete();
}
OutputStream outputStream = new FileOutputStream(targetFile);
while ((readLength = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, readLength);
}
outputStream.flush();
outputStream.close();
}
return targetFile;
}
/**
* 辅助ContentUri 通过 MIME 类型获取后缀(精准匹配,不受文件名伪造影响)
* @param context 上下文
* @param uri ContentUri
* @return 匹配的后缀(无匹配返回空字符串)
*/
private static String getSuffixFromContentUri(Context context, Uri uri) {
String mime = null;
try {
// 通过 ContentResolver 获取 Uri 对应的 MIME 类型(系统级匹配,最精准)
mime = context.getContentResolver().getType(uri);
LogUtils.d(TAG, "getSuffixFromContentUri获取MIME类型=" + mime);
if (mime == null || mime.isEmpty()) {
// MIME 为空,尝试解析文件名兜底
String fileName = getFileNameFromContentUri(context, uri);
return getSuffixFromFilePath(fileName);
}
// MIME 类型匹配后缀(优先完全匹配,再模糊匹配)
if (MIME_SUFFIX_MAP.containsKey(mime)) {
return MIME_SUFFIX_MAP.get(mime);
}
// 模糊匹配(如 image/* 匹配通用图片后缀默认png
if (mime.startsWith("image/")) {
return "png";
} else if (mime.startsWith("video/")) {
return "mp4";
} else if (mime.startsWith("audio/")) {
return "mp3";
} else if (mime.startsWith("application/")) {
return "pdf";
}
} catch (Exception e) {
LogUtils.e(TAG, "getSuffixFromContentUriMIME解析失败mime=" + mime, e);
}
// 所有方式失败解析Uri路径兜底
return getSuffixFromFilePath(uri.getPath());
}
/**
* 辅助:从 ContentUri 获取文件名MIME 解析失败时兜底)
* @param context 上下文
* @param uri ContentUri
* @return 文件名(失败返回空字符串)
*/
private static String getFileNameFromContentUri(Context context, Uri uri) {
Cursor cursor = null;
try {
String[] queryColumns = {MediaStore.MediaColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
return cursor.getString(nameIndex);
}
} catch (Exception e) {
LogUtils.e(TAG, "getFileNameFromContentUri查询失败", e);
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
LogUtils.e(TAG, "getFileNameFromContentUri关闭Cursor失败", e);
}
}
}
return "";
}
/**
* 辅助:从文件路径/文件名截取后缀(兜底方案,处理各种路径格式)
* @param path 文件路径/文件名
* @return 截取的后缀(无后缀返回空字符串)
*/
private static String getSuffixFromFilePath(String path) {
if (path == null || path.isEmpty()) {
return "";
}
// 处理路径中的分隔符(兼容 Windows/Android 路径格式)
path = path.replace("\\", "/");
// 取最后一个 "/" 后的文件名(避免路径包含 "." 导致误判)
int lastSepIndex = path.lastIndexOf("/");
if (lastSepIndex != -1 && lastSepIndex < path.length() - 1) {
path = path.substring(lastSepIndex + 1);
}
// 截取最后一个 "." 后的后缀(过滤无后缀/点开头/点结尾场景)
int lastDotIndex = path.lastIndexOf(".");
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == path.length() - 1) {
return "";
}
// 过滤后缀中的非法字符(仅保留字母/数字,避免特殊字符干扰)
String suffix = path.substring(lastDotIndex + 1).replaceAll("[^a-zA-Z0-9]", "");
// 限制后缀长度1-5位避免超长伪造后缀
return suffix.length() >= 1 && suffix.length() <= 5 ? suffix : "";
}
}

View File

@@ -4,228 +4,327 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.text.TextUtils;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.R;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* 基于Java7的BackgroundViewLinearLayout+ImageView保持原图比例居中平铺
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 18:01
* @Describe 背景图片视图控件(支持预览临时图片 + 外部刷新)
*/
public class BackgroundView extends RelativeLayout {
public static final String TAG = "BackgroundView";
// 新增:记录当前已缓存的图片路径
private String mCurrentCachedPath = "";
private Context mContext;
private LinearLayout mLlContainer; // 主容器LinearLayout
private ImageView mIvBackground; // 图片显示控件
private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高)
Context mContext;
private ImageView ivBackground;
private static String BACKGROUND_IMAGE_FOLDER = "Background";
private static String BACKGROUND_IMAGE_FILENAME = "current.data";
private static String BACKGROUND_IMAGE_PREVIEW_FILENAME = "current_preview.data";
private static String backgroundSourceFilePath;
private float imageAspectRatio = 1.0f; // 默认 1:1
// 标记当前是否处于预览状态
private boolean isPreviewMode = false;
// ====================================== 构造器Java7兼容 ======================================
public BackgroundView(Context context) {
super(context);
LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ===");
this.mContext = context;
initView();
}
// ====================================== 初始化 ======================================
private void initView() {
LogUtils.d(TAG, "=== initView 启动 ===");
// 1. 配置当前控件:全屏+透明
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
// 2. 初始化主容器LinearLayout
initLinearLayout();
// 3. 初始化ImageView
initImageView();
// 初始设置透明背景
setDefaultTransparentBackground();
LogUtils.d(TAG, "=== initView 完成 ===");
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
initView();
}
private void initLinearLayout() {
LogUtils.d(TAG, "=== initLinearLayout 启动 ===");
mLlContainer = new LinearLayout(mContext);
// 配置LinearLayout全屏+垂直方向+居中
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
);
mLlContainer.setLayoutParams(llParams);
mLlContainer.setOrientation(LinearLayout.VERTICAL);
mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中
mLlContainer.setBackgroundColor(0x00000000);
this.addView(mLlContainer);
LogUtils.d(TAG, "=== initLinearLayout 完成 ===");
void initView() {
initBackgroundImageView();
initBackgroundImagePath();
loadAndSetImageViewBackground();
}
private void initImageView() {
LogUtils.d(TAG, "=== initImageView 启动 ===");
mIvBackground = new ImageView(mContext);
// 配置ImageViewwrap_content+居中+透明背景
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
mIvBackground.setLayoutParams(ivParams);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺
mIvBackground.setBackgroundColor(0x00000000);
mLlContainer.addView(mIvBackground);
LogUtils.d(TAG, "=== initImageView 完成 ===");
private void initBackgroundImageView() {
ivBackground = new ImageView(mContext);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
ivBackground.setLayoutParams(layoutParams);
ivBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
this.addView(ivBackground);
}
public void loadBackgroundBean(BackgroundBean bean) {
loadBackgroundBean(bean, false);
}
public void loadBackgroundBean(BackgroundBean bean, boolean isRefresh) {
if (!bean.isUseBackgroundFile()) {
setDefaultTransparentBackground();
private void initBackgroundImagePath() {
File externalFilesDir = mContext.getExternalFilesDir(null);
if (externalFilesDir == null) {
LogUtils.e(TAG, "外置存储不可用,无法初始化背景图片路径");
return;
}
String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
if (!(new File(targetPath).exists())) {
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
return;
}
// 调用带路径判断的loadImage方法
if (isRefresh) {
App._mBitmapCacheUtils.removeCachedBitmap(targetPath);
App._mBitmapCacheUtils.cacheBitmap(targetPath);
}
loadImage(targetPath);
File backgroundDir = new File(externalFilesDir, BACKGROUND_IMAGE_FOLDER);
if (!backgroundDir.exists()) {
backgroundDir.mkdirs();
}
backgroundSourceFilePath = new File(backgroundDir, BACKGROUND_IMAGE_FILENAME).getAbsolutePath();
}
// ====================================== 对外方法 ======================================
/**
* 改造后添加路径判断路径更新时同步更新缓存缓存Bitmap为null时提示并加载透明背景
* @param imagePath 图片绝对路径
* 拷贝图片文件到背景资源目录(正式背景
*/
public void loadImage(String imagePath) {
LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ===");
if (TextUtils.isEmpty(imagePath)) {
setDefaultTransparentBackground();
public void saveToBackgroundSources(String srcBackgroundPath) {
initBackgroundImagePath();
if (backgroundSourceFilePath == null) {
LogUtils.e(TAG, "目标路径初始化失败,无法保存背景图片");
return;
}
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile()) {
LogUtils.e(TAG, "图片文件无效");
setDefaultTransparentBackground();
File srcFile = new File(srcBackgroundPath);
if (!srcFile.exists() || !srcFile.isFile()) {
LogUtils.e(TAG, String.format("源文件不存在或不是文件:%s", srcBackgroundPath));
return;
}
mIvBackground.setVisibility(View.GONE);
// ======================== 新增:路径判断逻辑 ========================
// 1. 路径未变化:直接使用缓存
if (imagePath.equals(mCurrentCachedPath)) {
Bitmap cachedBitmap = App._mBitmapCacheUtils.getCachedBitmap(imagePath);
// 核心修改判断缓存Bitmap是否为null
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
mIvBackground.setVisibility(View.VISIBLE);
return;
} else {
// 缓存Bitmap为空或已回收提示并加载透明背景
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath);
ToastUtils.show("全局位图缓存为空,无法加载图片");
setDefaultTransparentBackground();
File destFile = new File(backgroundSourceFilePath);
File destDir = destFile.getParentFile();
if (destDir != null && !destDir.exists()) {
boolean isDirCreated = destDir.mkdirs();
if (!isDirCreated) {
LogUtils.e(TAG, "目标目录创建失败:" + destDir.getAbsolutePath());
return;
}
}
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
App._mBitmapCacheUtils.removeCachedBitmap(mCurrentCachedPath);
LogUtils.d(TAG, "loadImage: 路径已更新,移除旧缓存 - " + mCurrentCachedPath);
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
LogUtils.d(TAG, String.format("文件拷贝成功:%s -> %s", srcBackgroundPath, backgroundSourceFilePath));
// 拷贝成功后,若处于预览模式则退出预览,加载正式背景
if (isPreviewMode) {
exitPreviewMode();
} else {
loadAndSetImageViewBackground();
}
} catch (Exception e) {
LogUtils.e(TAG, String.format("文件拷贝失败:%s", e.getMessage()), e);
if (destFile.exists()) {
destFile.delete();
LogUtils.d(TAG, "已删除损坏的目标文件");
}
} finally {
if (fis != null) {
try {
fis.close();
} catch (Exception e) {
LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (Exception e) {
LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage());
}
}
}
// ======================== 路径判断逻辑结束 ========================
// 无缓存/路径更新:走原有逻辑加载图片
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
}
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败");
ToastUtils.show("图片解码失败,无法加载");
setDefaultTransparentBackground();
return;
}
// 缓存新图片,并更新当前缓存路径记录
App._mBitmapCacheUtils.cacheBitmap(imagePath);
mCurrentCachedPath = imagePath;
LogUtils.d(TAG, "loadImage: 加载新图片并更新缓存 - " + imagePath);
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
adjustImageViewSize();
mIvBackground.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "=== loadImage 完成 ===");
}
// ====================================== 内部工具方法 ======================================
/**
* 【新增公共函数】预览临时图片(不修改正式背景文件)
* @param previewImagePath 临时预览图片的路径
*/
public void previewBackgroundImage(String previewImagePath) {
if (previewImagePath == null || previewImagePath.isEmpty()) {
LogUtils.e(TAG, "预览图片路径为空");
return;
}
File previewFile = new File(previewImagePath);
if (!previewFile.exists() || !previewFile.isFile()) {
LogUtils.e(TAG, "预览图片不存在或不是文件:" + previewImagePath);
return;
}
// 计算预览图片宽高比
if (!calculateImageAspectRatio(previewFile)) {
LogUtils.e(TAG, "预览图片尺寸无效,无法预览");
return;
}
// 压缩加载预览图片
Bitmap previewBitmap = decodeBitmapWithCompress(previewFile, 1080, 1920);
if (previewBitmap == null) {
LogUtils.e(TAG, "预览图片加载失败");
return;
}
// 设置预览图片到 ImageView
Drawable previewDrawable = new BitmapDrawable(mContext.getResources(), previewBitmap);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
ivBackground.setBackground(previewDrawable);
} else {
ivBackground.setBackgroundDrawable(previewDrawable);
}
// 调整 ImageView 尺寸以匹配预览图片宽高比
adjustImageViewSize();
isPreviewMode = true;
LogUtils.d(TAG, "进入预览模式,预览图片路径:" + previewImagePath);
}
/**
* 【新增公共函数】退出预览模式,恢复显示正式背景图片
*/
public void exitPreviewMode() {
if (isPreviewMode) {
loadAndSetImageViewBackground();
isPreviewMode = false;
LogUtils.d(TAG, "退出预览模式,恢复正式背景");
}
}
/**
* 公共函数:供外部类调用,重新加载正式背景图片(刷新显示)
*/
public void reloadBackgroundImage() {
LogUtils.d(TAG, "外部调用重新加载背景图片");
initBackgroundImagePath();
loadAndSetImageViewBackground();
// 若处于预览模式,退出预览
if (isPreviewMode) {
isPreviewMode = false;
}
}
/**
* 加载正式背景图片并设置到 ImageView
*/
private void loadAndSetImageViewBackground() {
if (backgroundSourceFilePath == null) {
setDefaultImageViewBackground();
return;
}
File backgroundFile = new File(backgroundSourceFilePath);
if (!backgroundFile.exists() || !backgroundFile.isFile()) {
LogUtils.e(TAG, "背景图片不存在:" + backgroundSourceFilePath);
setDefaultImageViewBackground();
return;
}
if (!calculateImageAspectRatio(backgroundFile)) {
setDefaultImageViewBackground();
return;
}
Bitmap bitmap = decodeBitmapWithCompress(backgroundFile, 1080, 1920);
if (bitmap == null) {
LogUtils.e(TAG, "图片加载失败,无法解析为 Bitmap");
setDefaultImageViewBackground();
return;
}
Drawable backgroundDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
ivBackground.setBackground(backgroundDrawable);
} else {
ivBackground.setBackgroundDrawable(backgroundDrawable);
}
adjustImageViewSize();
LogUtils.d(TAG, "ImageView 背景加载成功,宽高比:" + imageAspectRatio);
}
/**
* 计算图片宽高比(宽/高)
*/
private boolean calculateImageAspectRatio(File file) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int width = options.outWidth;
int height = options.outHeight;
if (width <= 0 || height <= 0) {
LogUtils.e(TAG, "图片尺寸无效");
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
if (imageWidth <= 0 || imageHeight <= 0) {
LogUtils.e(TAG, "图片尺寸无效:宽=" + imageWidth + ", 高=" + imageHeight);
return false;
}
mImageAspectRatio = (float) width / height;
LogUtils.d(TAG, "原图比例:" + mImageAspectRatio);
imageAspectRatio = (float) imageWidth / imageHeight;
return true;
} catch (Exception e) {
LogUtils.e(TAG, "计算比失败:" + e.getMessage());
LogUtils.e(TAG, "计算图片宽高比失败:" + e.getMessage());
return false;
}
}
/**
* 动态调整 ImageView 尺寸以匹配图片宽高比
*/
private void adjustImageViewSize() {
int parentWidth = getWidth();
int parentHeight = getHeight();
if (parentWidth == 0 || parentHeight == 0) {
post(new Runnable() {
@Override
public void run() {
adjustImageViewSize();
}
});
return;
}
int imageViewWidth, imageViewHeight;
if (imageAspectRatio >= 1.0f) { // 横图
imageViewWidth = Math.min(parentWidth, (int) (parentHeight * imageAspectRatio));
imageViewHeight = (int) (imageViewWidth / imageAspectRatio);
} else { // 竖图
imageViewHeight = Math.min(parentHeight, (int) (parentWidth / imageAspectRatio));
imageViewWidth = (int) (imageViewHeight * imageAspectRatio);
}
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivBackground.getLayoutParams();
layoutParams.width = imageViewWidth;
layoutParams.height = imageViewHeight;
ivBackground.setLayoutParams(layoutParams);
}
/**
* 带压缩的 Bitmap 解码(避免 OOM
*/
private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
@@ -235,59 +334,41 @@ public class BackgroundView extends RelativeLayout {
int scaleX = options.outWidth / maxWidth;
int scaleY = options.outHeight / maxHeight;
int inSampleSize = Math.max(scaleX, scaleY);
if (inSampleSize <= 0) inSampleSize = 1;
if (inSampleSize <= 0) {
inSampleSize = 1;
}
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
} catch (Exception e) {
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage());
LogUtils.e(TAG, "图片压缩加载失败:" + e.getMessage());
return null;
}
}
private void adjustImageViewSize() {
if (mLlContainer == null || mIvBackground == null) {
return;
}
int llWidth = mLlContainer.getWidth();
int llHeight = mLlContainer.getHeight();
if (llWidth != 0 && llHeight != 0) {
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
ivHeight = (int) (ivWidth / mImageAspectRatio);
} else {
ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight);
ivWidth = (int) (ivHeight * mImageAspectRatio);
}
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER);
mIvBackground.setVisibility(View.VISIBLE);
}
/**
* 设置默认背景(图片加载失败时兜底)
*/
private void setDefaultImageViewBackground() {
ivBackground.setBackgroundResource(R.drawable.default_background);
imageAspectRatio = 1.0f;
adjustImageViewSize();
LogUtils.d(TAG, "已设置 ImageView 默认背景");
}
private void setDefaultTransparentBackground() {
mIvBackground.setImageBitmap(null);
mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f;
// 清空缓存路径记录
mCurrentCachedPath = "";
mIvBackground.setVisibility(View.GONE);
}
// ====================================== 重写方法 ======================================
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
adjustImageViewSize(); // 尺寸变化时重新调整
adjustImageViewSize();
}
/**
* 对外提供:判断当前是否处于预览模式
*/
public boolean isPreviewMode() {
return isPreviewMode;
}
}

View File

@@ -0,0 +1,31 @@
<?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">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<cc.winboll.studio.libaes.views.AToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/root_ll"/>
</LinearLayout>

View File

@@ -6,34 +6,39 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libaes.views.ASupportToolbar
<cc.winboll.studio.libaes.views.AToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activitybackgroundpictureRelativeLayout1"/>
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/background_view">
android:background="#FF7381FF"
android:id="@+id/activitybackgroundpictureBackgroundView1">
</cc.winboll.studio.powerbell.views.BackgroundView>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="400dp">
android:layout_height="wrap_content"
android:layout_below="@id/toolbar">
<RelativeLayout
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<cc.winboll.studio.libaes.views.AButton

View File

@@ -5,13 +5,6 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<!-- 搜索框:提示文本改为“搜索应用名称或包名” -->
<EditText

View File

@@ -6,11 +6,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libaes.views.ASupportToolbar
<cc.winboll.studio.libaes.views.AToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<LinearLayout

View File

@@ -1,259 +1,40 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 顶部Toolbar首屏核心同步加载保留原有ASupportToolbar -->
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<!-- 主内容区(优化层级,减少冗余RelativeLayout -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<!-- 首屏核心容器(合并原冗余RelativeLayout,减少层级) -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activitymainRelativeLayout1">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activitymainRelativeLayout1"
android:background="#FFB7B7B7"/>
<!-- 1. 背景视图(首屏核心,同步加载,保留原有) -->
<cc.winboll.studio.powerbell.views.BackgroundView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragmentmainviewBackgroundView1"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activitymainFrameLayout1"/>
<!-- 2. 功能控件容器(首屏核心,同步加载,保留原有结构) -->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</RelativeLayout>
<!-- 服务总开关布局 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentmainviewLinearLayout3"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="@drawable/bg_frame">
<Switch
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewSwitch1"
android:padding="10dp"
android:layout_weight="1.0"
android:textSize="@dimen/text_title_size"/>
</LinearLayout>
</LinearLayout>
<!-- 电量控制核心布局SeekBar+图标) -->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1.0">
<!-- 耗电提醒布局 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:id="@+id/fragmentmainviewLinearLayout1">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/usege"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:id="@+id/fragmentmainviewCheckBox2"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar2"
android:progressTint="@color/colorUsege"
android:progressBackgroundTint="@color/colorUsege"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout>
<!-- 耗电提醒数值+图标 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView3"
android:gravity="center_horizontal"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView2"
android:layout_weight="1.0"/>
</LinearLayout>
<!-- 当前电量数值+图标 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView4"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView1"
android:layout_weight="1.0"/>
</LinearLayout>
<!-- 充电提醒数值+图标 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView2"
android:gravity="center_horizontal"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView3"
android:layout_weight="1.0"/>
</LinearLayout>
<!-- 充电提醒布局 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp"
android:id="@+id/fragmentmainviewLinearLayout2">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/charge"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:id="@+id/fragmentmainviewCheckBox1"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar1"
android:progressTint="@color/colorCharge"
android:progressBackgroundTint="@color/colorCharge"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- Tips文本 -->
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tips"
android:textSize="@dimen/text_content_size"
android:id="@+id/fragmentandroidviewTextView1"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>
</LinearLayout>
<!-- 3. 广告视图关键优化→用ViewStub延迟加载替代原直接加载的ADsBannerView -->
<!-- 首次启动仅占位1px不inflate真实广告视图减少首次耗时 -->
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/stub_ads_banner"
android:layout_alignParentBottom="true"
android:layout="@layout/view_ads_banner"/> <!-- 广告视图独立布局文件 -->
</RelativeLayout>
</RelativeLayout>
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"/>
</LinearLayout>

View File

@@ -5,47 +5,11 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:orientation="vertical"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF0C6BBF">
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/backgroundview"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#AF4FDA4E">
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main"
android:id="@+id/btn_main_activity"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TestCropImage"
android:id="@+id/btn_test_cropimage"/>
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
android:id="@+id/activitymainunittestFrameLayout1"/>
</LinearLayout>

View File

@@ -25,11 +25,11 @@
android:layout_height="wrap_content"
android:gravity="center_vertical|center_horizontal">
<cc.winboll.studio.powerbell.views.BackgroundView
android:orientation="vertical"
android:layout_width="200dp"
android:layout_height="200dp"
android:id="@+id/backgroundview"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:id="@+id/dialogbackgroundpicturepreviewImageView1"/>
</LinearLayout>

View File

@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF7381FF"
android:id="@+id/fragmentmainviewBackgroundView1"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentmainviewLinearLayout3"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="@drawable/bg_frame">
<Switch
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewSwitch1"
android:padding="10dp"
android:layout_weight="1.0"
android:textSize="@dimen/text_title_size"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1.0">
<LinearLayout
android:orientation="vertical"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:id="@+id/fragmentmainviewLinearLayout1">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/usege"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:id="@+id/fragmentmainviewCheckBox2"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar2"
android:progressTint="@color/colorUsege"
android:progressBackgroundTint="@color/colorUsege"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView3"
android:gravity="center_horizontal"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView2"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView4"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView1"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="@dimen/text_title_size"
android:layout_gravity="center_horizontal"
android:id="@+id/fragmentandroidviewTextView2"
android:gravity="center_horizontal"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewImageView3"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp"
android:id="@+id/fragmentmainviewLinearLayout2">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/charge"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:id="@+id/fragmentmainviewCheckBox1"/>
<cc.winboll.studio.powerbell.views.VerticalSeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fragmentandroidviewVerticalSeekBar1"
android:progressTint="@color/colorCharge"
android:progressBackgroundTint="@color/colorCharge"
android:layout_weight="1.0"
android:layout_margin="10dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tips"
android:textSize="@dimen/text_content_size"
android:id="@+id/fragmentandroidviewTextView1"
android:background="@drawable/bg_frame"
android:padding="10dp"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<cc.winboll.studio.powerbell.views.BackgroundView
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:background="#FF7381FF">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main"
android:id="@+id/btn_main_activity"/>
</HorizontalScrollView>
</cc.winboll.studio.powerbell.views.BackgroundView>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 广告视图独立布局供ViewStub延迟加载 -->
<cc.winboll.studio.libaes.views.ADsBannerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"/>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/bg_main">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bg_imageview"/>
</RelativeLayout>

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