From 6fd86a2742f60c14e46ef4d2da11f1baafc31d18 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Mon, 29 Dec 2025 21:36:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0TTS=E8=B4=B4=E5=BF=83?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=EF=BC=8C=E4=BB=A5=E5=85=8D=E5=9C=A8=E5=85=85?= =?UTF-8?q?=E7=94=B5=E6=97=B6=E8=AE=BE=E7=BD=AE=E4=BA=86=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=8F=90=E9=86=92=E5=8D=B4=E4=B8=8D=E7=9F=A5=E9=81=93=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- powerbell/build.properties | 4 +- powerbell/src/main/AndroidManifest.xml | 123 +++++++-- .../activities/SettingsActivity.java | 110 ++++++++ .../powerbell/models/TTSSpeakTextBean.java | 47 ++++ .../models/ThoughtfulServiceBean.java | 156 +++++++++++ .../ControlCenterServiceReceiver.java | 10 + .../powerbell/services/TTSPlayService.java | 83 ++++++ .../powerbell/services/ThoughtfulService.java | 164 ++++++++++++ .../powerbell/utils/TextToSpeechUtils.java | 251 ++++++++++++++++++ .../src/main/res/drawable/bg_frame_white.xml | 41 +++ powerbell/src/main/res/drawable/speaker.xml | 11 + .../src/main/res/layout/activity_settings.xml | 41 ++- .../src/main/res/layout/view_tts_back.xml | 43 +++ 13 files changed, 1051 insertions(+), 33 deletions(-) create mode 100644 powerbell/src/main/java/cc/winboll/studio/powerbell/models/TTSSpeakTextBean.java create mode 100644 powerbell/src/main/java/cc/winboll/studio/powerbell/models/ThoughtfulServiceBean.java create mode 100644 powerbell/src/main/java/cc/winboll/studio/powerbell/services/TTSPlayService.java create mode 100644 powerbell/src/main/java/cc/winboll/studio/powerbell/services/ThoughtfulService.java create mode 100644 powerbell/src/main/java/cc/winboll/studio/powerbell/utils/TextToSpeechUtils.java create mode 100644 powerbell/src/main/res/drawable/bg_frame_white.xml create mode 100644 powerbell/src/main/res/drawable/speaker.xml create mode 100644 powerbell/src/main/res/layout/view_tts_back.xml 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"/> -