diff --git a/powerbell/build.properties b/powerbell/build.properties
index e920641..17bb0f2 100644
--- a/powerbell/build.properties
+++ b/powerbell/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Sun Dec 28 20:43:38 HKT 2025
+#Mon Dec 29 13:34:00 GMT 2025
stageCount=41
libraryProject=
baseVersion=15.14
publishVersion=15.14.40
-buildCount=0
+buildCount=16
baseBetaVersion=15.14.41
diff --git a/powerbell/src/main/AndroidManifest.xml b/powerbell/src/main/AndroidManifest.xml
index 89e5bda..4d60df8 100644
--- a/powerbell/src/main/AndroidManifest.xml
+++ b/powerbell/src/main/AndroidManifest.xml
@@ -4,55 +4,84 @@
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
-
+
+
+
+
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
+
+
+
+
-
-
+
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
+
+
+
+
-
+
+
+
+
+
+
-
@@ -253,4 +327,3 @@
-
diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java
index 4832d85..e6c02e1 100644
--- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java
+++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java
@@ -1,12 +1,24 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
+import android.provider.Settings;
import android.view.View;
+import android.view.WindowManager;
+import android.widget.CheckBox;
+import android.widget.Toast;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
+import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
+import java.lang.reflect.Field;
/**
* 应用设置窗口,提供应用配置项的统一入口
@@ -44,6 +56,13 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
// 初始化工具栏
initToolbar();
+
+ ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
+ if (thoughtfulServiceBean == null) {
+ thoughtfulServiceBean = new ThoughtfulServiceBean();
+ }
+ ((CheckBox)findViewById(R.id.activitysettingsCheckBox1)).setChecked(thoughtfulServiceBean.isEnableUsePowerTts());
+ ((CheckBox)findViewById(R.id.activitysettingsCheckBox2)).setChecked(thoughtfulServiceBean.isEnableChargeTts());
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化完成");
}
@@ -70,5 +89,96 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
});
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
}
+
+ public void onCheckTTSDrawOverlaysPermission(View view) {
+ canDrawOverlays();
+ }
+
+ public void onEnableChargeTts(View view) {
+ ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
+ if (thoughtfulServiceBean == null) {
+ thoughtfulServiceBean = new ThoughtfulServiceBean();
+ }
+ thoughtfulServiceBean.setIsEnableChargeTts(((CheckBox)view).isChecked());
+ ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
+ }
+
+ public void onEnableUsePowerTts(View view) {
+ ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
+ if (thoughtfulServiceBean == null) {
+ thoughtfulServiceBean = new ThoughtfulServiceBean();
+ }
+ thoughtfulServiceBean.setIsEnableUsePowerTts(((CheckBox)view).isChecked());
+ ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
+ }
+
+ /**
+ * 悬浮窗权限检查与请求
+ */
+ void canDrawOverlays() {
+ LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
+ // API6.0+校验权限
+ if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
+ LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
+ showDrawOverlayRequestDialog();
+ } else {
+ ToastUtils.show("悬浮窗权限已开启");
+ }
+ }
+
+
+ /**
+ * 显示悬浮窗权限请求对话框
+ */
+ private void showDrawOverlayRequestDialog() {
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle("权限请求")
+ .setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
+ .setPositiveButton("去设置", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ jumpToDrawOverlaySettings();
+ }
+ })
+ .setNegativeButton("稍后", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .create();
+
+ // 解决对话框焦点问题
+ if (dialog.getWindow() != null) {
+ dialog.getWindow().setFlags(
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ }
+ dialog.show();
+ }
+
+ /**
+ * 跳转悬浮窗权限设置页面(反射适配低版本)
+ */
+ private void jumpToDrawOverlaySettings() {
+ LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
+ try {
+ // 反射获取设置页面Action(避免高版本API依赖)
+ Class> settingsClazz = Settings.class;
+ Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
+ String action = (String) actionField.get(null);
+
+ // 跳转当前应用权限设置页
+ Intent intent = new Intent(action);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setData(Uri.parse("package:" + getPackageName()));
+ startActivity(intent);
+ } catch (Exception e) {
+ LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
+ Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
+ }
+ }
+
}
diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/models/TTSSpeakTextBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/models/TTSSpeakTextBean.java
new file mode 100644
index 0000000..d9370fc
--- /dev/null
+++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/models/TTSSpeakTextBean.java
@@ -0,0 +1,47 @@
+package cc.winboll.studio.powerbell.models;
+
+import cc.winboll.studio.libappbase.LogUtils;
+import java.io.Serializable;
+
+/**
+ * TTS 语音播放文本内容实体类
+ * 适配:Java7 语法规范 | Android API30 系统版本
+ * 特性:实现序列化接口,支持跨页面/进程传递,属性默认值初始化
+ * @Author 豆包&ZhanGSKen
+ * @Date 2025/12/29 19:13
+ */
+public class TTSSpeakTextBean implements Serializable {
+
+ // ====================================== 常量区 - 置顶排序 ======================================
+ /** 日志TAG 瞬态修饰,不参与序列化,减少序列化体积 */
+ transient public static final String TAG = "TTSSpeakTextBean";
+
+ // ====================================== 成员属性区 - 业务属性排序 ======================================
+ /** 延迟播放时长 单位:毫秒,默认值0:无延迟播放 */
+ public int mnDelay = 0;
+ /** TTS语音播放文本内容,默认值空字符串:防止空指针 */
+ public String mszSpeakContent = "";
+
+ // ====================================== 构造方法区 - 无参+有参 完整实现 ======================================
+ /**
+ * 无参构造方法
+ * Java7序列化规范必备 + 兼容反射实例化场景
+ */
+ public TTSSpeakTextBean() {
+ LogUtils.d(TAG, "【无参构造】TTSSpeakTextBean 实例化,使用默认值 | 延迟:" + mnDelay + " | 文本:" + mszSpeakContent);
+ }
+
+ /**
+ * 有参构造方法【主构造】
+ * @param nDelay 延迟播放时长(ms)
+ * @param szSpeakContent 语音播放文本内容
+ */
+ public TTSSpeakTextBean(int nDelay, String szSpeakContent) {
+ LogUtils.d(TAG, "【有参构造】TTSSpeakTextBean 实例化,入参 | 延迟:" + nDelay + " | 文本:" + szSpeakContent);
+ this.mnDelay = nDelay;
+ this.mszSpeakContent = szSpeakContent;
+ LogUtils.d(TAG, "【有参构造】赋值完成 | 最终延迟:" + this.mnDelay + " | 最终文本:" + this.mszSpeakContent);
+ }
+
+}
+
diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/models/ThoughtfulServiceBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/models/ThoughtfulServiceBean.java
new file mode 100644
index 0000000..7332325
--- /dev/null
+++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/models/ThoughtfulServiceBean.java
@@ -0,0 +1,156 @@
+package cc.winboll.studio.powerbell.models;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+import cc.winboll.studio.libappbase.BaseBean;
+import cc.winboll.studio.libappbase.LogUtils;
+
+/**
+ * @Author 豆包&ZhanGSKen
+ * @Date 2025/12/29 20:59
+ * @Describe 贴心服务配置实体类 (适配API30 / Java7)
+ */
+public class ThoughtfulServiceBean extends BaseBean implements Parcelable, Serializable {
+
+ // ====================== 常量区 - 置顶统一管理 ======================
+ public static final String TAG = ThoughtfulServiceBean.class.getSimpleName();
+ private static final long serialVersionUID = 1L; // Serializable 序列化兼容必备
+ // JSON序列化字段常量 杜绝硬编码
+ public static final String JSON_FIELD_IS_ENABLE_CHARGE_TTS = "isEnableChargeTts";
+ public static final String JSON_FIELD_IS_ENABLE_USE_POWER_TTS = "isEnableUsePowerTts";
+
+ // ====================== 核心成员变量 - 私有封装 ======================
+ private boolean isEnableChargeTts = false; // 是否启用 充电TTS贴心语音服务
+ private boolean isEnableUsePowerTts = false; // 是否启用 用电TTS贴心语音服务
+
+ // ====================== Parcelable 静态创建器 (API30标准写法 必须public static final) ======================
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public ThoughtfulServiceBean createFromParcel(Parcel source) {
+ return new ThoughtfulServiceBean(source);
+ }
+
+ @Override
+ public ThoughtfulServiceBean[] newArray(int size) {
+ LogUtils.d(TAG, "newArray: 初始化数组,size = " + size);
+ return new ThoughtfulServiceBean[size];
+ }
+ };
+
+ // ====================== 构造方法区 (无参+有参+Parcel构造 全覆盖) ======================
+ /**
+ * 无参构造 - JSON解析/反射实例化 必备
+ */
+ public ThoughtfulServiceBean() {
+ LogUtils.d(TAG, "ThoughtfulServiceBean: 无参构造,初始化默认禁用所有TTS服务");
+ }
+
+ /**
+ * 全参构造 - 手动配置所有服务状态
+ * @param isEnableChargeTts 充电TTS服务开关
+ * @param isEnableUsePowerTts 用电TTS服务开关
+ */
+ public ThoughtfulServiceBean(boolean isEnableChargeTts, boolean isEnableUsePowerTts) {
+ this.isEnableChargeTts = isEnableChargeTts;
+ this.isEnableUsePowerTts = isEnableUsePowerTts;
+ LogUtils.d(TAG, "ThoughtfulServiceBean: 全参构造 | isEnableChargeTts=" + isEnableChargeTts + " | isEnableUsePowerTts=" + isEnableUsePowerTts);
+ }
+
+ /**
+ * Parcel反序列化构造 - Parcelable必备 私有私有化
+ */
+ private ThoughtfulServiceBean(Parcel in) {
+ this.isEnableChargeTts = in.readByte() != 0;
+ this.isEnableUsePowerTts = in.readByte() != 0;
+ LogUtils.d(TAG, "ThoughtfulServiceBean: Parcel构造解析完成 | isEnableChargeTts=" + isEnableChargeTts + " | isEnableUsePowerTts=" + isEnableUsePowerTts);
+ }
+
+ // ====================== Getter/Setter 方法区 (封装成员变量 统一访问) ======================
+ public boolean isEnableChargeTts() {
+ return isEnableChargeTts;
+ }
+
+ public void setIsEnableChargeTts(boolean isEnableChargeTts) {
+ LogUtils.d(TAG, "setIsEnableChargeTts: 旧值=" + this.isEnableChargeTts + " | 新值=" + isEnableChargeTts);
+ this.isEnableChargeTts = isEnableChargeTts;
+ }
+
+ public boolean isEnableUsePowerTts() {
+ return isEnableUsePowerTts;
+ }
+
+ public void setIsEnableUsePowerTts(boolean isEnableUsePowerTts) {
+ LogUtils.d(TAG, "setIsEnableUsePowerTts: 旧值=" + this.isEnableUsePowerTts + " | 新值=" + isEnableUsePowerTts);
+ this.isEnableUsePowerTts = isEnableUsePowerTts;
+ }
+
+ // ====================== 重写父类 BaseBean 核心方法 (JSON序列化/反序列化 业务核心) ======================
+ @Override
+ public String getName() {
+ String className = ThoughtfulServiceBean.class.getName();
+ LogUtils.d(TAG, "getName: 返回当前实体类名 = " + className);
+ return className;
+ }
+
+ /**
+ * JSON序列化 - 写入所有字段 适配持久化/网络传输
+ */
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name(JSON_FIELD_IS_ENABLE_CHARGE_TTS).value(this.isEnableChargeTts);
+ jsonWriter.name(JSON_FIELD_IS_ENABLE_USE_POWER_TTS).value(this.isEnableUsePowerTts);
+ LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成,所有TTS服务状态已写入");
+ }
+
+ /**
+ * JSON反序列化 - 读取字段生成实体 适配数据恢复
+ */
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ ThoughtfulServiceBean bean = new ThoughtfulServiceBean();
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String fieldName = jsonReader.nextName();
+ switch (fieldName) {
+ case JSON_FIELD_IS_ENABLE_CHARGE_TTS:
+ bean.setIsEnableChargeTts(jsonReader.nextBoolean());
+ break;
+ case JSON_FIELD_IS_ENABLE_USE_POWER_TTS:
+ bean.setIsEnableUsePowerTts(jsonReader.nextBoolean());
+ break;
+ default:
+ jsonReader.skipValue();
+ LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段 = " + fieldName);
+ break;
+ }
+ }
+ jsonReader.endObject();
+ LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成,生成实体对象");
+ return bean;
+ }
+
+ // ====================== 实现 Parcelable 接口方法 (组件间Intent传递必备 API30/Java7完美适配) ======================
+ @Override
+ public int describeContents() {
+ return 0; // 无文件描述符等特殊内容,固定返回0即可
+ }
+
+ /**
+ * Parcel序列化 - boolean用byte存储(Java7/API30标准写法 避免兼容性问题)
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (isEnableChargeTts ? 1 : 0));
+ dest.writeByte((byte) (isEnableUsePowerTts ? 1 : 0));
+ LogUtils.d(TAG, "writeToParcel: Parcel序列化完成,所有TTS服务状态已写入");
+ }
+
+}
+
diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/receivers/ControlCenterServiceReceiver.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/receivers/ControlCenterServiceReceiver.java
index 7a97938..3fc81e8 100644
--- a/powerbell/src/main/java/cc/winboll/studio/powerbell/receivers/ControlCenterServiceReceiver.java
+++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/receivers/ControlCenterServiceReceiver.java
@@ -13,6 +13,7 @@ import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import java.lang.ref.WeakReference;
+import cc.winboll.studio.powerbell.services.ThoughtfulService;
/**
* 控制中心广播接收器
@@ -110,6 +111,15 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
LogUtils.d(TAG, "handleBatteryStateChanged() 跳过 | 电池状态无变化");
return;
}
+
+ // 在插拔充电线时,执行贴心服务
+ if(currentCharging != sIsCharging) {
+ if(currentCharging) {
+ ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.CHARGE_STATE);
+ } else {
+ ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.DISCHARGE_STATE);
+ }
+ }
// 3. 更新静态缓存状态,保证多线程可见
sIsCharging = currentCharging;
diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/services/TTSPlayService.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/services/TTSPlayService.java
new file mode 100644
index 0000000..55c09d8
--- /dev/null
+++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/services/TTSPlayService.java
@@ -0,0 +1,83 @@
+package cc.winboll.studio.powerbell.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.powerbell.models.TTSSpeakTextBean;
+import cc.winboll.studio.powerbell.utils.TextToSpeechUtils;
+import java.util.ArrayList;
+
+/**
+ * TTS 语音播放后台服务组件
+ * 适配:Java7 语法规范 | Android API30 系统版本
+ * 功能:后台承载TTS语音播放,解耦页面生命周期,避免页面销毁中断播放
+ * @Author 豆包&ZhanGSKen
+ * @Date 2025/12/29 19:12
+ */
+public class TTSPlayService extends Service {
+
+ // ====================================== 常量区 - 静态全局常量 置顶排序 ======================================
+ public static final String TAG = "TTSPlayService";
+ public static final String EXTRA_SPEAKDATA = "EXTRA_SPEAKDATA";
+
+ // ====================================== 对外公开静态快捷调用方法【新增核心】======================================
+ /**
+ * 公开静态方法:一键启动TTS播放服务,播放指定文本内容
+ * @param context 上下文对象
+ * @param speakText 需要播放的语音文本内容
+ */
+ public static void startPlayTTS(Context context, String speakText) {
+ LogUtils.d(TAG, "【startPlayTTS】静态快捷调用方法 | 入参Context=" + context + " | 播放文本=" + speakText);
+ if (context != null && speakText != null && !speakText.isEmpty()) {
+ // 初始化播放数据集合
+ ArrayList ttsBeanList = new ArrayList<>();
+ // 添加播放文本,延迟时间为0:无延迟立即播放
+ ttsBeanList.add(new TTSSpeakTextBean(0, speakText));
+ LogUtils.d(TAG, "【startPlayTTS】封装播放数据完成,创建启动服务意图");
+
+ // 创建意图并封装序列化参数
+ Intent intent = new Intent(context, TTSPlayService.class);
+ intent.putExtra(EXTRA_SPEAKDATA, ttsBeanList);
+
+ // 启动当前服务
+ context.startService(intent);
+ LogUtils.d(TAG, "【startPlayTTS】已调用startService,TTS播放服务启动成功");
+ } else {
+ LogUtils.d(TAG, "【startPlayTTS】上下文为空 或 播放文本为空/空字符串,跳过启动服务");
+ }
+ }
+
+ // ====================================== 生命周期方法 - 绑定服务 (无绑定逻辑) ======================================
+ @Override
+ public IBinder onBind(Intent intent) {
+ LogUtils.d(TAG, "【onBind】服务绑定方法调用,入参Intent:" + intent);
+ return null;
+ }
+
+ // ====================================== 生命周期方法 - 启动服务【核心方法】 ======================================
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtils.d(TAG, "【onStartCommand】服务启动方法调用 | 入参Intent:" + intent + " | flags:" + flags + " | startId:" + startId);
+ // 解析播放数据并执行播放
+ if (intent != null) {
+ LogUtils.d(TAG, "【onStartCommand】Intent不为空,开始解析序列化播放数据");
+ ArrayList listTTSSpeakTextBean = (ArrayList) intent.getSerializableExtra(EXTRA_SPEAKDATA);
+ if (listTTSSpeakTextBean != null && listTTSSpeakTextBean.size() > 0) {
+ LogUtils.d(TAG, "【onStartCommand】解析播放数据成功,队列长度:" + listTTSSpeakTextBean.size() + ",调用TTS播放工具类");
+ TextToSpeechUtils.getInstance(this).speekTTSList(listTTSSpeakTextBean);
+ } else {
+ LogUtils.d(TAG, "【onStartCommand】播放数据为空/长度0,跳过语音播放逻辑");
+ }
+ } else {
+ LogUtils.d(TAG, "【onStartCommand】Intent为空,无播放数据可解析");
+ }
+ // 返回默认值,保持原服务启动策略不变
+ int result = super.onStartCommand(intent, flags, startId);
+ LogUtils.d(TAG, "【onStartCommand】方法执行完成,返回值:" + result);
+ return result;
+ }
+
+}
+
diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/services/ThoughtfulService.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/services/ThoughtfulService.java
new file mode 100644
index 0000000..79c87bb
--- /dev/null
+++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/services/ThoughtfulService.java
@@ -0,0 +1,164 @@
+package cc.winboll.studio.powerbell.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.powerbell.models.AppConfigBean;
+import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
+import cc.winboll.studio.powerbell.utils.AppConfigUtils;
+
+/**
+ * 智能电池服务(充电/放电状态处理)
+ * 适配:Java7 语法规范 | Android API30 系统版本
+ * 功能:接收充电/放电状态指令,根据不同状态执行对应业务任务
+ * @Author 豆包&ZhanGSKen
+ * @Date 2025/12/29 19:29
+ */
+public class ThoughtfulService extends Service {
+
+ // ====================================== 常量区 - 置顶排序 ======================================
+ public static final String TAG = "ThoughtfulService";
+ /** Intent传递 服务类型 的Key值 */
+ public static final String EXTRA_SERVICE_TYPE = "EXTRA_SERVICE_TYPE";
+
+ // ====================================== 枚举类 - 服务类型 充电/放电状态 ======================================
+ /**
+ * 服务执行类型枚举
+ * CHARGE_STATE : 充电状态服务
+ * DISCHARGE_STATE : 放电(耗电)状态服务
+ */
+ public enum ServiceType {
+ CHARGE_STATE, //充电状态服务
+ DISCHARGE_STATE //放电状态服务
+ }
+
+ // ====================================== 对外公开静态启动函数【新增核心】入参Context + 枚举 ======================================
+ /**
+ * 公开静态方法:传入上下文+服务类型枚举,一键构建意图并启动当前服务
+ * @param context 上下文对象
+ * @param serviceType 服务类型枚举【充电/放电】
+ */
+ public static void startServiceWithType(Context context, ServiceType serviceType) {
+ LogUtils.d(TAG, "【startServiceWithType】静态启动方法调用 | Context=" + context + " | ServiceType=" + (serviceType == null ? "null" : serviceType.name()));
+
+ ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(context, ThoughtfulServiceBean.class);
+ if (thoughtfulServiceBean == null) {
+ thoughtfulServiceBean = new ThoughtfulServiceBean();
+ }
+
+ // 对应TTS服务提醒没有启用,就退出
+ if((serviceType == ServiceType.CHARGE_STATE && !thoughtfulServiceBean.isEnableChargeTts())
+ ||(serviceType == ServiceType.DISCHARGE_STATE && !thoughtfulServiceBean.isEnableUsePowerTts())){
+ return;
+ }
+
+
+ // 判空健壮性校验
+ if (context != null && serviceType != null) {
+ // 构建意图 + 封装枚举参数
+ Intent intent = new Intent(context, ThoughtfulService.class);
+ intent.putExtra(EXTRA_SERVICE_TYPE, serviceType);
+ // 启动服务
+ context.startService(intent);
+ LogUtils.d(TAG, "【startServiceWithType】服务启动成功,执行[" + serviceType.name() + "]任务");
+ } else {
+ LogUtils.d(TAG, "【startServiceWithType】上下文为空 或 服务类型枚举为空,跳过启动服务");
+ }
+ }
+
+ // ====================================== 生命周期方法 - 绑定服务 (原逻辑保留) ======================================
+ @Override
+ public IBinder onBind(Intent intent) {
+ LogUtils.d(TAG, "【onBind】服务绑定方法调用,入参Intent:" + intent);
+ return null;
+ }
+
+ // ====================================== 生命周期方法 - 启动服务【核心逻辑】接收枚举+分支执行任务 ======================================
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtils.d(TAG, "【onStartCommand】服务启动方法调用 | intent=" + intent + " | flags=" + flags + " | startId=" + startId);
+ // 判断意图非空,解析服务类型参数
+ if (intent != null) {
+ LogUtils.d(TAG, "【onStartCommand】Intent不为空,开始解析服务类型枚举参数");
+ // 获取传递的服务类型枚举
+ ServiceType serviceType = (ServiceType) intent.getSerializableExtra(EXTRA_SERVICE_TYPE);
+ // 根据服务类型,执行对应任务
+ if (serviceType != null) {
+ LogUtils.d(TAG, "【onStartCommand】解析到服务类型:" + serviceType.name());
+ switch (serviceType) {
+ case CHARGE_STATE:
+ // 执行【充电状态】对应的业务任务
+ executeChargeStateTask();
+ break;
+ case DISCHARGE_STATE:
+ // 执行【放电状态】对应的业务任务
+ executeDischargeStateTask();
+ break;
+ default:
+ LogUtils.d(TAG, "【onStartCommand】未知的服务类型,不执行任何任务");
+ break;
+ }
+ } else {
+ LogUtils.d(TAG, "【onStartCommand】未解析到有效服务类型参数,参数为空");
+ }
+ } else {
+ LogUtils.d(TAG, "【onStartCommand】启动服务的Intent为空,直接返回");
+ }
+
+ // 返回默认策略,与原生逻辑一致
+ int result = super.onStartCommand(intent, flags, startId);
+ LogUtils.d(TAG, "【onStartCommand】服务执行完成,返回值:" + result);
+ return result;
+ }
+
+ // ====================================== 私有业务方法 充电/放电 分任务执行 ======================================
+ /**
+ * 执行【充电状态】的业务任务
+ * 可在此方法内编写 充电时的逻辑(语音提醒/电量监控/弹窗等)
+ */
+ private void executeChargeStateTask() {
+ LogUtils.d(TAG, "【executeChargeStateTask】执行【充电状态】业务任务 >>> ");
+ //ToastUtils.show("【executeChargeStateTask】执行【充电状态】业务任务 >>> ");
+ // TODO 此处添加充电状态需要执行的业务逻辑代码
+ // 加载最新配置
+ AppConfigBean latestConfig = AppConfigUtils.getInstance(this).loadAppConfig();
+ if (latestConfig == null) {
+ LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
+ return;
+ }
+
+ if (latestConfig.isEnableChargeReminder()) {
+ int nChargeReminderValue = latestConfig.getChargeReminderValue();
+ String szRemind = String.format("额定充电提醒已启用,额定值为百分之%d。", nChargeReminderValue);
+ szRemind = szRemind + szRemind + szRemind;
+ TTSPlayService.startPlayTTS(this, szRemind);
+ }
+ }
+
+ /**
+ * 执行【放电(耗电)状态】的业务任务
+ * 可在此方法内编写 放电时的逻辑(语音提醒/电量监控/弹窗等)
+ */
+ private void executeDischargeStateTask() {
+ LogUtils.d(TAG, "【executeDischargeStateTask】执行【放电状态】业务任务 >>> ");
+ //ToastUtils.show("【executeDischargeStateTask】执行【放电状态】业务任务 >>> ");
+ // TODO 此处添加放电状态需要执行的业务逻辑代码
+ // 加载最新配置
+ AppConfigBean latestConfig = AppConfigUtils.getInstance(this).loadAppConfig();
+ if (latestConfig == null) {
+ LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
+ return;
+ }
+
+ if (latestConfig.isEnableUsageReminder()) {
+ int nUsageReminderValue = latestConfig.getUsageReminderValue();
+ String szRemind = String.format("电量不足提醒已启用,低电值为百分之%d。", nUsageReminderValue);
+ //szRemind = szRemind + szRemind + szRemind;
+ TTSPlayService.startPlayTTS(this, szRemind);
+ }
+ }
+
+}
+
diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/TextToSpeechUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/TextToSpeechUtils.java
new file mode 100644
index 0000000..54757eb
--- /dev/null
+++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/TextToSpeechUtils.java
@@ -0,0 +1,251 @@
+package cc.winboll.studio.powerbell.utils;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.powerbell.R;
+import cc.winboll.studio.powerbell.models.TTSSpeakTextBean;
+import java.util.ArrayList;
+
+/**
+ * TTS语音播放工具类 (单例实现)
+ * 适配:Java7 语法规范 | Android API36 系统版本【修复崩溃】
+ * 功能:队列播放语音文本 + 播放悬浮窗展示 + 点击悬浮窗停止播放/关闭悬浮窗
+ * @Author 豆包&ZhanGSKen
+ * @Date 2025/12/29 19:03
+ */
+public class TextToSpeechUtils {
+
+ // ====================================== 常量区 - 静态全局常量 (置顶) ======================================
+ public static final String TAG = "TextToSpeechUtils";
+ public static final String UNIQUE_ID = "UNIQUE_ID";
+
+ // ====================================== 单例实例 - 静态私有 (饿汉式优化) ======================================
+ private static volatile TextToSpeechUtils sTextToSpeechUtils;
+
+ // ====================================== 成员属性区 - 私有成员变量 (按功能归类 有序排列) ======================================
+ private Context mContext;
+ private WindowManager mWindowManager;
+ private TextToSpeech mTextToSpeech;
+ private View mView;
+ private volatile boolean isExist = false;
+ private UtteranceProgressListener mUtteranceProgressListener;
+
+ // ====================================== 构造方法 - 私有私有化 (单例模式) ======================================
+ private TextToSpeechUtils(Context context) {
+ LogUtils.d(TAG, "【构造方法】初始化TextToSpeechUtil实例");
+ this.mContext = context.getApplicationContext();
+ this.mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ this.initUtteranceProgressListener();
+ LogUtils.d(TAG, "【构造方法】初始化完成,获取WindowManager实例:"+mWindowManager);
+ }
+
+ // ====================================== 对外暴露方法 - 单例获取入口 (线程安全) ======================================
+ public static synchronized TextToSpeechUtils getInstance(Context context) {
+ LogUtils.d(TAG, "【getInstance】获取单例实例,入参Context:" + context);
+ if (sTextToSpeechUtils == null) {
+ LogUtils.d(TAG, "【getInstance】实例为空,创建新的TextToSpeechUtil对象");
+ sTextToSpeechUtils = new TextToSpeechUtils(context);
+ }
+ return sTextToSpeechUtils;
+ }
+
+ // ====================================== 核心对外业务方法 - 播放TTS语音队列 【主入口】 ======================================
+ public void speekTTSList(final ArrayList listTTSSpeakTextBean) {
+ LogUtils.d(TAG, "【speekTTSList】播放语音队列调用,入参队列长度:" + (listTTSSpeakTextBean == null ? 0 : listTTSSpeakTextBean.size()));
+ // 重置播放退出标志位
+ isExist = false;
+ LogUtils.d(TAG, "【speekTTSList】重置播放退出标志位 isExist = " + isExist);
+
+ // TTS实例为空 → 初始化TTS后重放
+ if (mTextToSpeech == null) {
+ LogUtils.d(TAG, "【speekTTSList】TextToSpeech实例为空,开始初始化TTS");
+ mTextToSpeech = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
+ @Override
+ public void onInit(int initStatus) {
+ LogUtils.d(TAG, "【onInit】TTS初始化回调,初始化状态码:" + initStatus);
+ if (initStatus == TextToSpeech.SUCCESS) {
+ LogUtils.d(TAG, "【onInit】TTS初始化成功,重新调用语音播放方法");
+ speekTTSList(listTTSSpeakTextBean);
+ } else {
+ LogUtils.d(TAG, "【onInit】TTS init failed : " + initStatus + ". The app [https://play.google.com/store/apps/details?id=com.google.android.tts] maybe fix this TTS probrem. ");
+ }
+ }
+ });
+ mTextToSpeech.setOnUtteranceProgressListener(mUtteranceProgressListener);
+ LogUtils.d(TAG, "【speekTTSList】已为TTS绑定播放进度监听器");
+ } else {
+ // TTS实例就绪 → 执行播放逻辑
+ if (listTTSSpeakTextBean != null && listTTSSpeakTextBean.size() > 0) {
+ LogUtils.d(TAG, "【speekTTSList】TTS实例就绪,语音队列数据有效,开始播放逻辑处理");
+ // 清理过期的悬浮窗 - 防止内存泄漏/重复添加
+ clearFloatWindow();
+
+ // ========== 修复1:添加悬浮窗权限检查,有权限才初始化悬浮窗,无权限则只播语音不崩溃 ==========
+ if (checkOverlayPermission()) {
+ initWindow();
+ LogUtils.d(TAG, "【speekTTSList】悬浮窗初始化并显示完成");
+ } else {
+ LogUtils.d(TAG, "【speekTTSList】悬浮窗权限未授予,跳过悬浮窗显示,仅播放语音");
+ }
+
+ // 获取第一条语音的延迟时间并休眠
+ int nDelay = listTTSSpeakTextBean.get(0).mnDelay;
+ LogUtils.d(TAG, "【speekTTSList】获取播放延迟时间:" + nDelay + "ms,开始休眠等待");
+ try {
+ Thread.sleep(nDelay);
+ } catch (InterruptedException e) {
+ LogUtils.d(TAG, "【speekTTSList】休眠等待被中断", e);
+ }
+ LogUtils.d(TAG, "【speekTTSList】休眠等待完成,开始循环播放语音队列");
+
+ // 循环播放语音队列
+ for (int speakPosition = 0; speakPosition < listTTSSpeakTextBean.size() && !isExist; speakPosition++) {
+ String szSpeakContent = listTTSSpeakTextBean.get(speakPosition).mszSpeakContent;
+ isExist = (listTTSSpeakTextBean.size() - 2 < speakPosition);
+ LogUtils.d(TAG, "【speekTTSList】播放索引:" + speakPosition + " | 播放文本:" + szSpeakContent + " | 当前退出标记位:" + isExist);
+
+ // 第一条语音清空队列播放,后续语音追加播放
+ if (speakPosition == 0) {
+ mTextToSpeech.speak(szSpeakContent, TextToSpeech.QUEUE_FLUSH, null, UNIQUE_ID);
+ LogUtils.d(TAG, "【speekTTSList】执行清空队列播放 → QUEUE_FLUSH");
+ } else {
+ mTextToSpeech.speak(szSpeakContent, TextToSpeech.QUEUE_ADD, null, UNIQUE_ID);
+ LogUtils.d(TAG, "【speekTTSList】执行追加队列播放 → QUEUE_ADD");
+ }
+ }
+ LogUtils.d(TAG, "【speekTTSList】语音队列循环播放逻辑执行完毕");
+ } else {
+ LogUtils.d(TAG, "【speekTTSList】语音队列为空/长度0,跳过播放逻辑");
+ }
+ }
+ }
+
+ // ====================================== 私有工具方法 - 初始化播放监听器 ======================================
+ private void initUtteranceProgressListener() {
+ LogUtils.d(TAG, "【initUtteranceProgressListener】初始化TTS播放进度监听器");
+ mUtteranceProgressListener = new UtteranceProgressListener() {
+ @Override
+ public void onStart(String utteranceId) {
+ LogUtils.d(TAG, "【onStart】TTS语音播放开始,唯一标识ID:" + utteranceId);
+ }
+
+ @Override
+ public void onDone(String utteranceId) {
+ LogUtils.d(TAG, "【onDone】TTS语音播放结束,唯一标识ID:" + utteranceId + " | 退出标志位:" + isExist);
+ // 播放完成 关闭悬浮窗
+ if (isExist && mWindowManager != null && mView != null) {
+ LogUtils.d(TAG, "【onDone】满足关闭条件,执行悬浮窗移除操作");
+ clearFloatWindow();
+ }
+ }
+
+ @Override
+ public void onError(String utteranceId) {
+ LogUtils.d(TAG, "【onError】TTS语音播放出错,唯一标识ID:" + utteranceId);
+ }
+ };
+ }
+
+ // ====================================== 私有核心方法 - 初始化并添加悬浮窗 【核心修复 根治崩溃】 ======================================
+ private void initWindow() {
+ LogUtils.d(TAG, "【initWindow】开始初始化播放悬浮窗");
+ // 创建Window布局参数
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+ // ========== 修复2 重中之重:Android 12(API31)+ 彻底废弃TYPE_PHONE,统一用TYPE_APPLICATION_OVERLAY 适配API36 ==========
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+ LogUtils.d(TAG, "【initWindow】系统版本>=API26,悬浮窗类型:TYPE_APPLICATION_OVERLAY");
+ } else {
+ // 仅低版本用TYPE_PHONE,高版本不再走这里
+ params.type = WindowManager.LayoutParams.TYPE_PHONE;
+ LogUtils.d(TAG, "【initWindow】系统版本= Build.VERSION_CODES.M) {
+ boolean hasPermission = android.provider.Settings.canDrawOverlays(mContext);
+ LogUtils.d(TAG, "【checkOverlayPermission】Android6.0+ 悬浮窗权限校验结果:" + hasPermission);
+ return hasPermission;
+ } else {
+ // 低版本默认有权限
+ return true;
+ }
+ }
+
+ // ====================================== ✅ 新增:释放资源方法【根治内存泄漏】建议在Service/Activity销毁时调用 ✅ ======================================
+ public void release() {
+ LogUtils.d(TAG, "【release】释放TTS资源和悬浮窗");
+ clearFloatWindow();
+ if (mTextToSpeech != null) {
+ mTextToSpeech.stop();
+ mTextToSpeech.shutdown();
+ mTextToSpeech = null;
+ }
+ sTextToSpeechUtils = null;
+ }
+}
+
diff --git a/powerbell/src/main/res/drawable/bg_frame_white.xml b/powerbell/src/main/res/drawable/bg_frame_white.xml
new file mode 100644
index 0000000..002ffe1
--- /dev/null
+++ b/powerbell/src/main/res/drawable/bg_frame_white.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/powerbell/src/main/res/drawable/speaker.xml b/powerbell/src/main/res/drawable/speaker.xml
new file mode 100644
index 0000000..53e00c1
--- /dev/null
+++ b/powerbell/src/main/res/drawable/speaker.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/powerbell/src/main/res/layout/activity_settings.xml b/powerbell/src/main/res/layout/activity_settings.xml
index 52beb1a..4344895 100644
--- a/powerbell/src/main/res/layout/activity_settings.xml
+++ b/powerbell/src/main/res/layout/activity_settings.xml
@@ -14,17 +14,46 @@
style="@style/DefaultAToolbar"/>
-
+
+
+ android:gravity="right">
+
+
+
+
diff --git a/powerbell/src/main/res/layout/view_tts_back.xml b/powerbell/src/main/res/layout/view_tts_back.xml
new file mode 100644
index 0000000..5020d77
--- /dev/null
+++ b/powerbell/src/main/res/layout/view_tts_back.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+