slambb 2 місяців тому
батько
коміт
41c1088e3b

+ 1 - 1
.idea/deploymentTargetSelector.xml

@@ -7,7 +7,7 @@
       </SelectionState>
       <SelectionState runConfigName="app">
         <option name="selectionMode" value="DROPDOWN" />
-        <DropdownSelection timestamp="2026-02-27T09:09:46.048179Z">
+        <DropdownSelection timestamp="2026-02-28T07:36:53.133345Z">
           <Target type="DEFAULT_BOOT">
             <handle>
               <DeviceId pluginId="PhysicalDevice" identifier="serial=0123456789ABCDEF" />

+ 2 - 2
app/build.gradle

@@ -11,8 +11,8 @@ android {
         minSdkVersion 21
         targetSdkVersion 32 //建议此属性值设为21 io.dcloud.PandoraEntry 作为apk入口时   必须设置 targetSDKVersion>=21 沉浸式才生效
 
-        versionCode 26022702
-        versionName "2.3.16"
+        versionCode 26031205
+        versionName "2.3.26"
         multiDexEnabled true
         ndk {
             abiFilters 'x86','x86_64','armeabi', 'armeabi-v7a','arm64-v8a'

+ 2 - 0
app/src/main/AndroidManifest.xml

@@ -151,6 +151,8 @@
         android:normalScreens="true"
         android:resizeable="true"
         android:smallScreens="true" />
+    <uses-permission android:name="android.permission.DEVICE_POWER" />
+
     <uses-feature android:name="android.hardware.camera" />
 
     <uses-feature android:name="android.hardware.camera.autofocus" />

+ 1 - 1
app/src/main/assets/apps/__UNI__8D02B4B/www/androidPrivacy.json

@@ -1,3 +1,3 @@
 {
-    "prompt": "template"
+    "prompt" : "none"
 }

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
app/src/main/assets/apps/__UNI__8D02B4B/www/app-config-service.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
app/src/main/assets/apps/__UNI__8D02B4B/www/app-service.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
app/src/main/assets/apps/__UNI__8D02B4B/www/app-view.js


+ 1 - 1
app/src/main/assets/apps/__UNI__8D02B4B/www/manifest.json

@@ -1 +1 @@
-{"@platforms":["android","iPhone","iPad"],"id":"__UNI__8D02B4B","name":"Heart_App","version":{"name":"2.3.16","code":26022702},"description":"一款用于运动心率监控的app,同时有丰富的卡路里计算和配餐功能。","launch_path":"__uniappview.html","developer":{"name":"","email":"","url":""},"permissions":{"Bluetooth":{},"VideoPlayer":{},"Maps":{"coordType":"gcj02"},"Geolocation":{},"UniNView":{"description":"UniNView原生渲染"}},"plus":{"useragent":{"value":"uni-app","concatenate":true},"splashscreen":{"target":"id:1","autoclose":true,"waiting":true,"delay":0},"popGesture":"close","launchwebview":{"id":"1","kernel":"WKWebview"},"statusbar":{"immersed":"supportedDevice","style":"dark","background":"#000000"},"usingComponents":true,"nvueStyleCompiler":"uni-app","compilerVersion":3,"runmode":"liberate","uniStatistics":{"enable":false},"allowsInlineMediaPlayback":true,"safearea":{"background":"#FEF6E1","bottom":{"offset":"auto"}},"uni-app":{"compilerVersion":"4.66","control":"uni-v3","nvueCompiler":"uni-app","renderer":"auto","nvue":{"flex-direction":"column"},"nvueLaunchMode":"normal"},"tabBar":{"color":"#AAABAD","selectedColor":"#9797FF","backgroundColor":"#FEF6E1","borderStyle":"#eee","height":"75px","spacing":"6px","list":[{"pagePath":"pages/personal-page/home-train/home-train","text":"","iconPath":"static/common/navBar/train.png","selectedIconPath":"static/common/navBar/trainB.png"},{"pagePath":"pages/personal-page/trainClassify/trainClassify","text":"","iconPath":"static/common/navBarNew/video@2x.png","selectedIconPath":"static/common/navBarNew/videoB@2x.png"},{"pagePath":"pages/personal-page/home-tool/home-tool","text":"","iconPath":"static/common/navBarNew/tool@2x.png","selectedIconPath":"static/common/navBarNew/toolB@2x.png"},{"pagePath":"pages/personal-page/diet/diet","text":"","iconPath":"static/common/navBarNew/diet@2x.png","selectedIconPath":"static/common/navBarNew/dietB@2x.png"}]},"launch_path":"__uniappview.html"}}
+{"@platforms":["android","iPhone","iPad"],"id":"__UNI__8D02B4B","name":"Heart_App","version":{"name":"2.3.26","code":26031205},"description":"一款用于运动心率监控的app,同时有丰富的卡路里计算和配餐功能。","launch_path":"__uniappview.html","developer":{"name":"","email":"","url":""},"permissions":{"Bluetooth":{},"VideoPlayer":{},"Maps":{"coordType":"gcj02"},"Geolocation":{},"UniNView":{"description":"UniNView原生渲染"}},"plus":{"useragent":{"value":"uni-app","concatenate":true},"splashscreen":{"target":"id:1","autoclose":true,"waiting":true,"delay":0},"popGesture":"close","launchwebview":{"id":"1","kernel":"WKWebview"},"statusbar":{"immersed":"supportedDevice","style":"dark","background":"#000000"},"usingComponents":true,"nvueStyleCompiler":"uni-app","compilerVersion":3,"runmode":"liberate","uniStatistics":{"enable":false},"allowsInlineMediaPlayback":true,"safearea":{"background":"#FEF6E1","bottom":{"offset":"auto"}},"uni-app":{"compilerVersion":"4.66","control":"uni-v3","nvueCompiler":"uni-app","renderer":"auto","nvue":{"flex-direction":"column"},"nvueLaunchMode":"normal"},"tabBar":{"color":"#AAABAD","selectedColor":"#9797FF","backgroundColor":"#FEF6E1","borderStyle":"#eee","height":"75px","spacing":"6px","list":[{"pagePath":"pages/personal-page/home-train/home-train","text":"","iconPath":"static/common/navBar/train.png","selectedIconPath":"static/common/navBar/trainB.png"},{"pagePath":"pages/personal-page/trainClassify/trainClassify","text":"","iconPath":"static/common/navBarNew/video@2x.png","selectedIconPath":"static/common/navBarNew/videoB@2x.png"},{"pagePath":"pages/personal-page/home-tool/home-tool","text":"","iconPath":"static/common/navBarNew/tool@2x.png","selectedIconPath":"static/common/navBarNew/toolB@2x.png"},{"pagePath":"pages/personal-page/diet/diet","text":"","iconPath":"static/common/navBarNew/diet@2x.png","selectedIconPath":"static/common/navBarNew/dietB@2x.png"}]},"launch_path":"__uniappview.html"}}

BIN
app/src/main/assets/apps/__UNI__8D02B4B/www/static/splash.jpg


+ 43 - 1
app/src/main/java/com/YuyeTech/HeartRate/KioskHelper.java

@@ -17,6 +17,31 @@ public class KioskHelper {
     
     private static final String TAG = "KioskHelper";
     
+    /**
+     * 仅用 Context 禁用锁屏(Device Owner 时生效)。
+     * 用于在 Application 或 BootReceiver 中尽早调用,避免开机自启时处于锁屏阶段导致
+     * 输入法(IME)无法挂载或显示、被 Keyguard 层级遮挡的问题。
+     * 调用时机:Application.onCreate() 或 BOOT_COMPLETED/LOCKED_BOOT_COMPLETED 收到时。
+     */
+    public static void disableKeyguardIfOwner(Context context) {
+        if (context == null) {
+            Log.w(TAG, "Context is null, cannot disable keyguard");
+            return;
+        }
+        try {
+            Context app = context.getApplicationContext();
+            DevicePolicyManager dpm = (DevicePolicyManager)
+                    app.getSystemService(Context.DEVICE_POLICY_SERVICE);
+            ComponentName adminComponent = new ComponentName(app, KioskDeviceAdminReceiver.class);
+            if (dpm != null && dpm.isDeviceOwnerApp(app.getPackageName())) {
+                dpm.setKeyguardDisabled(adminComponent, true);
+                Log.d(TAG, "✅ 锁屏已禁用(Application/开机阶段)");
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "disableKeyguardIfOwner 失败", e);
+        }
+    }
+    
     /**
      * 禁用锁屏和解锁屏幕(在 Activity 创建时调用)
      */
@@ -56,7 +81,7 @@ public class KioskHelper {
     }
     
     /**
-     * 保持屏幕常亮
+     * 保持屏幕常亮(常亮会加大耗电,临时方案下不调用)
      */
     public static void keepScreenOn(Activity activity) {
         if (activity == null) {
@@ -74,6 +99,23 @@ public class KioskHelper {
         }
     }
     
+    /**
+     * 不设置常亮:清除 FLAG_KEEP_SCREEN_ON,由系统休眠时间(如 50 分钟)控制息屏,降低耗电。
+     * 临时方案:在 Pad 系统设置中提前设置休眠时间(如 50 分钟),再安装运行 App。
+     */
+    public static void clearKeepScreenOn(Activity activity) {
+        if (activity == null) {
+            Log.w(TAG, "Activity is null, cannot clear keep screen on");
+            return;
+        }
+        try {
+            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+            Log.d(TAG, "✅ 已关闭常亮,跟随系统休眠时间");
+        } catch (Exception e) {
+            Log.e(TAG, "清除常亮标志失败", e);
+        }
+    }
+    
     /**
      * 启用锁屏(恢复系统默认行为)
      */

+ 12 - 3
app/src/main/java/com/YuyeTech/HeartRate/MyApplication.java

@@ -46,6 +46,9 @@ public class MyApplication extends DCloudApplication {
         
         if (BuildConfig.DEBUG) Log.d(TAG, "Application 初始化");
 
+        // ✅ 尽早禁用锁屏(Device Owner):避免开机自启时仍在锁屏阶段导致 IME 无法显示/被遮挡
+        KioskHelper.disableKeyguardIfOwner(this);
+
 		// 启动时重置 WiFi 检测开关,避免上一次运行遗留为 true 导致一进来就弹框
 		try {
 			getSharedPreferences("kiosk_prefs", Context.MODE_PRIVATE)
@@ -65,6 +68,7 @@ public class MyApplication extends DCloudApplication {
         LaunchConfig.setLaunchActivity(PandoraEntryActivity.class);
         
         // ✅ 启用 Debug 模式(发布时改为 false)
+        //KioskManager.setDebug(true);
         //KioskManager.setDebug(BuildConfig.DEBUG);
         
         // ✅ 注册屏幕状态广播接收器(监听电源键)
@@ -81,12 +85,15 @@ public class MyApplication extends DCloudApplication {
                     
                     // ✅ 先禁用锁屏(必须在 attach 之前)
                     KioskHelper.disableLockScreen(activity);
-                    KioskHelper.keepScreenOn(activity);
                     
-                    // ✅ 然后绑定 KioskManager
-                    KioskManager.setDebug(BuildConfig.DEBUG);
+                    // 临时方案:不常亮,由系统休眠时间(如 50 分钟)控制;需在 attach 之前设置
+                    KioskManager.setAllowKeepScreenOn(false);
+                    //KioskManager.setDebug(BuildConfig.DEBUG);
                     KioskManager.attach(activity);
                     
+                    //KioskHelper.clearKeepScreenOn(activity);
+                    PowerSavingManager.ensureWakeLockReleased();
+                    
                     // ✅ 拦截返回键,防止回到黑屏页面
                     interceptBackPress(activity);
                     
@@ -108,6 +115,8 @@ public class MyApplication extends DCloudApplication {
                 if (activity instanceof io.dcloud.PandoraEntry || 
                     activity instanceof io.dcloud.PandoraEntryActivity) {
                     KioskManager.onResume();
+                    // 临时方案:确保未加常亮(若 KioskManager.setAllowKeepScreenOn(false) 已设则此处为双保险)
+                    //KioskHelper.clearKeepScreenOn(activity);
                     
                     // ✅ 更新 PowerSavingManager 的 Activity 引用
                     PowerSavingManager.setCurrentActivity(activity);

+ 28 - 27
app/src/main/java/com/YuyeTech/HeartRate/PowerSavingManager.java

@@ -7,6 +7,8 @@ import android.util.Log;
 import android.view.Window;
 import android.view.WindowManager;
 
+import io.dcloud.uniplugin.HeartRateModule;
+
 import java.lang.ref.WeakReference;
 
 /**
@@ -26,6 +28,9 @@ public class PowerSavingManager {
     // 省电模式状态标志
     private static boolean isPowerSavingMode = false;
     
+    /** 临时方案:false = 不设置常亮,由系统休眠时间(如 50 分钟)控制;true = 恢复原常亮逻辑 */
+    private static boolean allowKeepScreenOn = false;
+    
     // 保存的屏幕亮度(用于恢复)
     private static float savedScreenBrightness = -1;
     
@@ -53,10 +58,10 @@ public class PowerSavingManager {
             releaseWakeLock();
             
             // 2. 降低屏幕亮度至最低(如果 Activity 可用)
-            dimScreenBrightness();
+            //dimScreenBrightness();
             
             // 3. 停止 UI 刷新(通过移除 FLAG_KEEP_SCREEN_ON)
-            stopUIRefresh();
+            //stopUIRefresh();
             
             // 4. 关闭外设(蓝牙扫描等)
             stopPeripherals(context);
@@ -83,10 +88,10 @@ public class PowerSavingManager {
         
         try {
             // 1. 恢复屏幕亮度
-            restoreScreenBrightness();
+            //restoreScreenBrightness();
             
             // 2. 恢复 UI 刷新(重新设置 FLAG_KEEP_SCREEN_ON)
-            resumeUIRefresh();
+            //resumeUIRefresh();
             
             // 3. 恢复外设(蓝牙扫描等)
             resumePeripherals(context);
@@ -98,6 +103,14 @@ public class PowerSavingManager {
         }
     }
     
+    /**
+     * 关闭 WakeLock(对外接口)
+     * 应用启动或界面创建时调用,确保不持有 WakeLock,降低息屏后耗电。
+     */
+    public static void ensureWakeLockReleased() {
+        releaseWakeLock();
+    }
+    
     /**
      * 释放 WakeLock
      * 允许 CPU 进入休眠状态
@@ -202,9 +215,13 @@ public class PowerSavingManager {
     
     /**
      * 恢复 UI 刷新
-     * 重新设置 FLAG_KEEP_SCREEN_ON,保持屏幕常亮
+     * 临时方案下(allowKeepScreenOn=false)不恢复常亮,由系统休眠时间控制息屏
      */
     private static void resumeUIRefresh() {
+        if (!allowKeepScreenOn) {
+            Log.d(TAG, "临时方案:不恢复 FLAG_KEEP_SCREEN_ON,跟随系统休眠");
+            return;
+        }
         try {
             Activity activity = getCurrentActivity();
             if (activity == null) {
@@ -213,9 +230,7 @@ public class PowerSavingManager {
             }
             
             Window window = activity.getWindow();
-            // 重新设置 FLAG_KEEP_SCREEN_ON,保持屏幕常亮
             window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-            
             Log.d(TAG, "✅ UI 刷新已恢复(已重新设置 FLAG_KEEP_SCREEN_ON)");
             
         } catch (Exception e) {
@@ -225,39 +240,25 @@ public class PowerSavingManager {
     
     /**
      * 停止外设
-     * 关闭蓝牙扫描、停止心跳检测等
+     * 通过 HeartRateModule 释放语音唤醒、停止播报、销毁 BLE 音频,并通知 UniApp(PowerSavingCallbacks)
      */
     private static void stopPeripherals(Context context) {
         try {
-            // TODO: 在这里添加停止蓝牙扫描、停止心跳检测等逻辑
-            // 例如:
-            // - 停止蓝牙扫描
-            // - 断开蓝牙连接
-            // - 停止摄像头
-            // - 停止其他高耗能组件
-            
+            HeartRateModule.stopForPowerSaving();
             Log.d(TAG, "✅ 外设已停止");
-            
         } catch (Exception e) {
             Log.e(TAG, "停止外设失败", e);
         }
     }
-    
+
     /**
      * 恢复外设
-     * 重新启动蓝牙扫描、心跳检测等
+     * 通过 HeartRateModule 向 UniApp 发送 powerSavingPeripheralsResumed 事件,由 JS 端恢复唤醒与 BLE
      */
     private static void resumePeripherals(Context context) {
         try {
-            // TODO: 在这里添加恢复蓝牙扫描、心跳检测等逻辑
-            // 例如:
-            // - 重新启动蓝牙扫描
-            // - 重新连接蓝牙设备
-            // - 重新启动摄像头
-            // - 重新启动其他高耗能组件
-            
-            Log.d(TAG, "✅ 外设已恢复");
-            
+            HeartRateModule.resumeForPowerSaving();
+            Log.d(TAG, "✅ 外设已恢复(已通知 UniApp)");
         } catch (Exception e) {
             Log.e(TAG, "恢复外设失败", e);
         }

+ 16 - 0
app/src/main/java/com/YuyeTech/HeartRate/ScreenStateReceiver.java

@@ -19,10 +19,26 @@ import android.util.Log;
 public class ScreenStateReceiver extends BroadcastReceiver {
     
     private static final String TAG = "ScreenStateReceiver";
+
+    /**
+     * 去抖:部分机型/场景下 ACTION_SCREEN_OFF/ON 可能在极短时间内连发(例如 OFF→ON→OFF→ON),
+     * 这里做节流,避免重复进入/退出省电模式导致业务抖动。
+     */
+    private static final long DEBOUNCE_MS = 500;
+    private static long lastActionTimeMs = 0;
+    private static String lastAction = null;
     
     @Override
     public void onReceive(Context context, Intent intent) {
         String action = intent.getAction();
+        Log.i(TAG, "********************* onReceive *********************");
+        long now = android.os.SystemClock.elapsedRealtime();
+        if (action != null && action.equals(lastAction) && (now - lastActionTimeMs) < DEBOUNCE_MS) {
+            Log.d(TAG, "⏭️ 屏幕广播去抖:忽略重复 action=" + action + " Δ=" + (now - lastActionTimeMs) + "ms");
+            return;
+        }
+        lastAction = action;
+        lastActionTimeMs = now;
         
         if (Intent.ACTION_SCREEN_OFF.equals(action)) {
             Log.i(TAG, "🔋 检测到屏幕关闭(用户按下电源键)→ 进入省电模式");

+ 5 - 4
app/src/main/java/com/YuyeTech/HeartRate/WifiCheckHelper.java

@@ -30,8 +30,9 @@ public class WifiCheckHelper {
     private static Runnable sCheckRunnable = null;
     private static Activity sCurrentActivity = null;
     
-    // 定时检测间隔:5秒
-    private static final long CHECK_INTERVAL_MS = 5000;
+    // 定时检测间隔:首次弹窗后再次提示的间隔(第二次及以后)
+    // 需求:首次弹出没问题,第二次弹出时间增加(例如第一次 5 秒,第二次保持 20 秒)
+    private static final long CHECK_INTERVAL_MS = 20000;
     
     /**
      * 检查 WiFi 连接状态
@@ -301,7 +302,7 @@ public class WifiCheckHelper {
         
         sCurrentActivity = activity;
         
-        if (DEBUG) Log.d(TAG, "🔄 启动定时检测(每5秒检测一次)");
+        if (DEBUG) Log.d(TAG, "🔄 启动定时检测(每" + (CHECK_INTERVAL_MS / 1000) + "秒检测一次)");
         
         sCheckRunnable = new Runnable() {
             @Override
@@ -336,7 +337,7 @@ public class WifiCheckHelper {
             }
         };
         
-        // 延迟5秒后开始第一次检测
+        // 延迟后开始第一次检测
         sCheckHandler.postDelayed(sCheckRunnable, CHECK_INTERVAL_MS);
     }
     

+ 17 - 6
myLockView/src/main/java/com/ble/mylockview/admin/KioskManager.java

@@ -66,6 +66,16 @@ public final class KioskManager {
     // 是否在临时退出期间需要拦截按键(用于安装 APK 等场景)
     private static boolean interceptKeysDuringTemporaryExit = false;
 
+    /** 是否允许保持屏幕常亮(FLAG_KEEP_SCREEN_ON)。false 时由系统休眠时间控制息屏,用于省电临时方案。 */
+    private static boolean allowKeepScreenOn = true;
+
+    /**
+     * 设置是否保持屏幕常亮。设为 false 时 Kiosk 不再添加 FLAG_KEEP_SCREEN_ON,由系统休眠时间(如 50 分钟)控制息屏。
+     * 需在 attach 之前调用。
+     */
+    public static void setAllowKeepScreenOn(boolean allow) {
+        allowKeepScreenOn = allow;
+    }
 
     /* ================== 对外 API ================== */
 
@@ -270,11 +280,12 @@ public final class KioskManager {
                         | View.SYSTEM_UI_FLAG_LOW_PROFILE
         );
 
-        window.addFlags(
-                WindowManager.LayoutParams.FLAG_FULLSCREEN
-                        | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
-                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
-        );
+        int flags = WindowManager.LayoutParams.FLAG_FULLSCREEN
+                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+        if (allowKeepScreenOn) {
+            flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+        }
+        window.addFlags(flags);
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             window.setDecorFitsSystemWindows(false);
@@ -358,7 +369,7 @@ public final class KioskManager {
 
     public static void setDebug(boolean debug) {
         debugMode = debug;
-        Log.w(TAG, "kiosk 设置模式"+debugMode);
+        Log.w(TAG, "kiosk 设置模式 debugMode:"+debugMode);
     }
 
     /* ================== 管理员解锁 ================== */

+ 55 - 3
uniplugin_module/src/main/java/io/dcloud/uniplugin/HeartRateModule.java

@@ -73,9 +73,17 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
     String TAG = "HeartRateModule";
     public static int REQUEST_CODE = 1000;
 
+    /** 静态引用,供 PowerSavingManager 在屏灭/屏亮时调用并 fireGlobalEventCallback 通知 UniApp */
+    private static volatile HeartRateModule sInstance;
+
     private TextToSpeech textToSpeech;//系统语音播报系统
     private boolean isSuccess = true;
 
+    public HeartRateModule() {
+        // UniModule 通常在同一进程内只会实例化一次;这里统一注册,避免散落在各方法里重复赋值
+        sInstance = this;
+    }
+
 
     /**
      * 百度语音
@@ -90,7 +98,15 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
     //百度语音在线合成
 //    protected MyTTs myTTs;
 
-
+    @UniJSMethod(uiThread = false)
+    public JSONObject SLA_InitBridge() {
+        Log.i(TAG, "SLA_InitBridge");
+        JSONObject data = new JSONObject();
+        data.put("code", 0);
+        data.put("bSuccess", isSuccess);
+        data.put("msg", "SLA_InitBridge success" );
+        return data;
+    }
     /**
      * 开启Android本地语音播报
      * @return
@@ -258,8 +274,44 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
     @UniJSMethod(uiThread = true)
     public void SLA_BDS_ReleaseWakeup() {
         // 基于DEMO唤醒词集成第5 退出事件管理器
-        myWakeup.release();
+        if (myWakeup != null) {
+            myWakeup.release();
+        }
+    }
 
+    /**
+     * 省电模式:停止外设(由 PowerSavingManager.stopPeripherals 在屏灭时调用)
+     * 仅向 UniApp 发送事件(PowerSavingCallbacks),具体资源释放由 JS 侧统一处理。
+     */
+    public static void stopForPowerSaving() {
+        if (sInstance == null) return;
+        try {
+            if (sInstance.mUniSDKInstance == null) return;
+            JSONObject data = new JSONObject();
+            data.put("type", "powerSavingPeripheralsStopped");
+            data.put("reason", "screenOff");
+            sInstance.mUniSDKInstance.fireGlobalEventCallback("PowerSavingCallbacks", data);
+            Log.d("HeartRateModule", "fireGlobalEventCallback: powerSavingPeripheralsStopped");
+        } catch (Exception e) {
+            Log.e("HeartRateModule", "fire powerSavingPeripheralsStopped error", e);
+        }
+    }
+
+    /**
+     * 省电模式:恢复外设通知(由 PowerSavingManager.resumePeripherals 在屏亮时调用)
+     * 仅向 UniApp 发送事件,具体恢复逻辑由 JS 端(appResume / 恢复唤醒与 BLE)处理。
+     */
+    public static void resumeForPowerSaving() {
+        if (sInstance == null || sInstance.mUniSDKInstance == null) return;
+        try {
+            JSONObject data = new JSONObject();
+            data.put("type", "powerSavingPeripheralsResumed");
+            data.put("reason", "screenOn");
+            sInstance.mUniSDKInstance.fireGlobalEventCallback("PowerSavingCallbacks", data);
+            Log.d("HeartRateModule", "fireGlobalEventCallback: powerSavingPeripheralsResumed");
+        } catch (Exception e) {
+            Log.e("HeartRateModule", "fire powerSavingPeripheralsResumed error", e);
+        }
     }
 
     @Override
@@ -315,7 +367,6 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
     @UniJSMethod(uiThread = true)
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
     public void HeartBLE_Init() {
-
         DisplayMetrics metrics = mUniSDKInstance.getContext().getResources().getDisplayMetrics();
         int width = metrics.widthPixels;
         int height = metrics.heightPixels;
@@ -484,6 +535,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
         //mManager.startScan(mScanCallback);
         heartRateSdkManager.startBluetoothOperations();
         mIsScanning = true;
+        // 若启用下方 Toast:Uniapp 需在 onDeviceConnected/onStopScan 时调用 uni.hideToast(),否则会蒙层 15 秒挡住按钮;建议 duration 用较短值(如 2000)且前端 mask 慎重
         myToast("正在连接", SCAN_DURATION);
         scanRunnable = () -> {
             if (mIsScanning) {

+ 42 - 6
uniplugin_module/src/main/java/io/dcloud/uniplugin/db800/HeartRateSdkManager.java

@@ -47,6 +47,9 @@ import pub.devrel.easypermissions.EasyPermissions;
 public class HeartRateSdkManager {
 
    private static final String TAG = "HeartRateSdkManager";
+   private static final long PERMISSION_DEBOUNCE_MS = 1500;
+   private volatile long lastPermissionRequestAtMs = 0;
+   private volatile boolean isRequestingBluetoothPermissions = false;
 
    /**
     * 蓝牙适配器
@@ -167,6 +170,11 @@ public class HeartRateSdkManager {
          }
          String deviceName = result.getDevice().getName();
          if (deviceName != null && isNameInFilter(deviceName)) {
+            // Android 8.0+:仅对可连接设备发起连接,不可连接的(如已被其他设备连接)跳过
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !result.isConnectable()) {
+               Log.d(TAG, "Skip device (not connectable): " + deviceName + " [" + device.getAddress() + "]");
+               return;
+            }
             stopScan();
             scannerPacket = new ScannerPacket(result);
             connectToDevice(device); // 连接蓝牙设备
@@ -202,13 +210,41 @@ public class HeartRateSdkManager {
    // 动态请求权限
    private static final int REQUEST_BLUETOOTH_PERMISSIONS = 1;
    private void requestBluetoothPermissions() {
-      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-         ActivityCompat.requestPermissions(
-                 (Activity) mContext, // 请确保替换为正确的Activity实例
-                 new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT},
-                 REQUEST_BLUETOOTH_PERMISSIONS
-         );
+      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return;
+
+      // requestPermissions 必须在 Activity + 主线程上执行,且避免多处调用导致连发
+      if (!(mContext instanceof Activity)) {
+         Log.w(TAG, "requestBluetoothPermissions skipped: mContext is not Activity");
+         return;
       }
+      final Activity activity = (Activity) mContext;
+      if (activity.isFinishing() || activity.isDestroyed()) {
+         Log.w(TAG, "requestBluetoothPermissions skipped: activity finishing/destroyed");
+         return;
+      }
+
+      long now = android.os.SystemClock.elapsedRealtime();
+      if (isRequestingBluetoothPermissions || (now - lastPermissionRequestAtMs) < PERMISSION_DEBOUNCE_MS) {
+         Log.d(TAG, "requestBluetoothPermissions debounced");
+         return;
+      }
+      isRequestingBluetoothPermissions = true;
+      lastPermissionRequestAtMs = now;
+
+      handler.post(() -> {
+         try {
+            ActivityCompat.requestPermissions(
+                    activity,
+                    new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT},
+                    REQUEST_BLUETOOTH_PERMISSIONS
+            );
+         } catch (Exception e) {
+            Log.e(TAG, "requestBluetoothPermissions error", e);
+         } finally {
+            // 允许后续再次请求(仍有时间窗节流)
+            isRequestingBluetoothPermissions = false;
+         }
+      });
    }
 
    /**

Деякі файли не було показано, через те що забагато файлів було змінено