Ver Fonte

完善日志部分功能

slambb há 6 dias atrás
pai
commit
94014c2ee3

+ 1 - 1
.idea/deploymentTargetSelector.xml

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

+ 2 - 2
UpdateHelper/build.gradle

@@ -10,8 +10,8 @@ android {
         applicationId "com.yuyetech.updatehelper"
         applicationId "com.yuyetech.updatehelper"
         minSdkVersion 21
         minSdkVersion 21
         targetSdkVersion 32 // 与主项目保持一致
         targetSdkVersion 32 // 与主项目保持一致
-        versionCode 1
-        versionName "1.0"
+        versionCode 26042202
+        versionName "1.2"
 
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
     }

+ 7 - 1
UpdateHelper/src/main/AndroidManifest.xml

@@ -14,6 +14,10 @@
     <!-- 监听包替换广播 -->
     <!-- 监听包替换广播 -->
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
 
 
+    <!-- 锁屏之上显示 / 全屏唤醒(不同设备 ROM 可能需要额外授权) -->
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
+
     <application
     <application
         android:allowBackup="true"
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:icon="@mipmap/ic_launcher"
@@ -25,7 +29,9 @@
         <activity
         <activity
             android:name=".MainActivity"
             android:name=".MainActivity"
             android:exported="true"
             android:exported="true"
-            android:launchMode="singleTask"
+            android:showWhenLocked="true"
+            android:turnScreenOn="true"
+            android:launchMode="singleInstance"
             android:screenOrientation="portrait"
             android:screenOrientation="portrait"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:theme="@style/Theme.UpdateHelper">
             android:theme="@style/Theme.UpdateHelper">

+ 125 - 85
UpdateHelper/src/main/java/com/yuyetech/updatehelper/MainActivity.java

@@ -6,6 +6,9 @@ import android.content.ComponentName;
 import android.content.Context;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller;
+import android.database.Cursor;
+import android.net.Uri;
+import android.app.KeyguardManager;
 import android.os.Build;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.util.Log;
 import android.util.Log;
@@ -25,10 +28,10 @@ import com.ble.mylockview.admin.KioskDeviceAdminReceiver;
 import com.ble.mylockview.admin.KioskManager;
 import com.ble.mylockview.admin.KioskManager;
 
 
 import java.io.File;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStream;
+import java.util.Locale;
 
 
 /**
 /**
  * UpdateHelper 主 Activity
  * UpdateHelper 主 Activity
@@ -50,6 +53,7 @@ public class MainActivity extends AppCompatActivity {
     private Button backToMainButton;
     private Button backToMainButton;
     
     
     private String apkPath;
     private String apkPath;
+    private Uri apkUri;
     private String mainPackage;
     private String mainPackage;
     private PackageInstaller.Session currentSession;
     private PackageInstaller.Session currentSession;
     private int currentSessionId = -1;
     private int currentSessionId = -1;
@@ -61,6 +65,29 @@ public class MainActivity extends AppCompatActivity {
     @Override
     @Override
     protected void onCreate(Bundle savedInstanceState) {
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         super.onCreate(savedInstanceState);
+
+        // 锁屏之上显示 + 点亮屏幕(让安装界面直接覆盖在锁屏层上)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+            setShowWhenLocked(true);
+            setTurnScreenOn(true);
+
+            KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+            if (km != null) {
+                try {
+                    km.requestDismissKeyguard(this, null);
+                } catch (Exception ignored) {
+                    // 部分设备/策略下可能不允许消除锁屏
+                }
+            }
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        } else {
+            getWindow().addFlags(
+                    WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                            | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                            | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                            | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+            );
+        }
         
         
         // 全屏显示
         // 全屏显示
         //requestWindowFeature(Window.FEATURE_NO_TITLE);
         //requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -83,7 +110,10 @@ public class MainActivity extends AppCompatActivity {
         
         
         // 获取传递的参数
         // 获取传递的参数
         Intent intent = getIntent();
         Intent intent = getIntent();
-        // 支持两种方式传递参数:直接 extra 或通过 Intent data
+        // 支持两种方式传递参数:
+        // 1) Android 11+ 推荐:Intent data 传入 content:// Uri(带临时读权限)
+        // 2) 兼容旧逻辑:extra 传入 apk_path(文件路径字符串)
+        apkUri = intent.getData();
         apkPath = intent.getStringExtra(EXTRA_APK_PATH);
         apkPath = intent.getStringExtra(EXTRA_APK_PATH);
         if (apkPath == null || apkPath.isEmpty()) {
         if (apkPath == null || apkPath.isEmpty()) {
             apkPath = intent.getStringExtra("apk_path");
             apkPath = intent.getStringExtra("apk_path");
@@ -94,9 +124,9 @@ public class MainActivity extends AppCompatActivity {
             mainPackage = intent.getStringExtra("main_package");
             mainPackage = intent.getStringExtra("main_package");
         }
         }
         
         
-        if (apkPath == null || apkPath.isEmpty()) {
+        if ((apkUri == null) && (apkPath == null || apkPath.isEmpty())) {
             // Log.e(TAG, "APK 路径为空");
             // Log.e(TAG, "APK 路径为空");
-            showError("APK 路径为空");
+            showError("APK Uri / 路径为空");
             return;
             return;
         }
         }
         
         
@@ -330,7 +360,8 @@ public class MainActivity extends AppCompatActivity {
         );
         );
 
 
         // 设置安装标志
         // 设置安装标志
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            // 只有 Android 12 及以上才支持此方法
             params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
             params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
         }
         }
 
 
@@ -340,21 +371,12 @@ public class MainActivity extends AppCompatActivity {
 
 
         currentSession = installer.openSession(currentSessionId);
         currentSession = installer.openSession(currentSessionId);
 
 
-        InputStream in = null;
-        OutputStream out = null;
-        
         try {
         try {
-            // 打开 APK 文件
-            File apkFile = new File(apkPath);
-            if (!apkFile.exists()) {
-                throw new IOException("APK 文件不存在: " + apkPath);
-            }
-
-            long apkSize = apkFile.length();
-            // Log.d(TAG, "APK 文件大小: " + apkSize + " bytes (" + (apkSize / 1024 / 1024) + " MB)");
+            final long apkSize = resolveApkSizeBytes();
+            final boolean sizeKnown = apkSize > 0;
 
 
             // 在写入大文件前,建议系统进行垃圾回收(不保证立即执行)
             // 在写入大文件前,建议系统进行垃圾回收(不保证立即执行)
-            if (apkSize > 100 * 1024 * 1024) { // 大于 100MB
+            if (sizeKnown && apkSize > 100 * 1024 * 1024) { // 大于 100MB
                 System.gc(); // 建议垃圾回收
                 System.gc(); // 建议垃圾回收
                 try {
                 try {
                     Thread.sleep(100); // 给 GC 一点时间
                     Thread.sleep(100); // 给 GC 一点时间
@@ -365,54 +387,41 @@ public class MainActivity extends AppCompatActivity {
 
 
             runOnUiThread(() -> updateStatus("正在安装..."));
             runOnUiThread(() -> updateStatus("正在安装..."));
 
 
-            // 打开输入流和输出流
-            in = new FileInputStream(apkFile);
-            out = currentSession.openWrite("package", 0, apkSize);
-
-            // 使用较小的缓冲区(32KB)以减少内存占用,同时保持合理的性能
-            // 对于大文件,较小的缓冲区可以降低峰值内存使用
-            byte[] buffer = new byte[32768]; // 32KB 缓冲区(从 64KB 减小到 32KB)
-            long totalWritten = 0;
-            int c;
-            int lastPercent = -1;
-
-            while ((c = in.read(buffer)) != -1) {
-                out.write(buffer, 0, c);
-                totalWritten += c;
-
-                if (apkSize > 0) {
-                    int percent = (int) (totalWritten * 100 / apkSize);
-                    if (percent != lastPercent) {
-                        lastPercent = percent;
-                        // Log.d(TAG, "安装进度: " + percent + "%");
-                        
-                        final int finalPercent = percent;
-                        runOnUiThread(() -> {
-                            updateProgress(finalPercent);
-                        });
+            // 打开输入流(Android 11+ 优先使用 content:// Uri;否则兼容文件路径)
+            try (InputStream in = openApkInputStream();
+                 OutputStream out = currentSession.openWrite("package", 0, sizeKnown ? apkSize : -1)) {
+
+                // 使用较小的缓冲区(32KB)以减少内存占用,同时保持合理的性能
+                byte[] buffer = new byte[32768];
+                long totalWritten = 0;
+                int c;
+                int lastPercent = -1;
+
+                while ((c = in.read(buffer)) != -1) {
+                    out.write(buffer, 0, c);
+                    totalWritten += c;
+
+                    if (sizeKnown) {
+                        int percent = (int) (totalWritten * 100 / apkSize);
+                        if (percent != lastPercent) {
+                            lastPercent = percent;
+                            final int finalPercent = percent;
+                            runOnUiThread(() -> updateProgress(finalPercent));
+                        }
+                    } else {
+                        // size 未知时,简单做“写入中”的状态刷新,避免 UI 长时间无变化
+                        if ((totalWritten % (5L * 1024 * 1024)) < buffer.length) { // 每约 5MB 更新一次
+                            final long writtenBytes = totalWritten;
+                            runOnUiThread(() -> updateStatus(String.format(
+                                    Locale.getDefault(),
+                                    "正在写入安装包...(已写入 %.1fMB)",
+                                    writtenBytes / 1024f / 1024f
+                            )));
+                        }
                     }
                     }
                 }
                 }
-            }
 
 
-            // 确保数据写入磁盘
-            currentSession.fsync(out);
-            
-            // 关闭流
-            if (out != null) {
-                try {
-                    out.close();
-                } catch (IOException e) {
-                    // Log.w(TAG, "关闭输出流失败", e);
-                }
-                out = null;
-            }
-            if (in != null) {
-                try {
-                    in.close();
-                } catch (IOException e) {
-                    // Log.w(TAG, "关闭输入流失败", e);
-                }
-                in = null;
+                currentSession.fsync(out);
             }
             }
             
             
             // 再次建议 GC,释放文件流占用的内存
             // 再次建议 GC,释放文件流占用的内存
@@ -458,17 +467,11 @@ public class MainActivity extends AppCompatActivity {
             
             
         } catch (OutOfMemoryError e) {
         } catch (OutOfMemoryError e) {
             // 处理内存不足错误
             // 处理内存不足错误
-            String errorMsg = "内存不足,无法安装大文件。APK 大小: " + 
-                (new File(apkPath).length() / 1024 / 1024) + " MB";
+            String errorMsg = "内存不足,无法安装大文件。";
             // Log.e(TAG, errorMsg, e);
             // Log.e(TAG, errorMsg, e);
             
             
-            // 清理资源
-            cleanupResources(in, out);
             throw new IOException(errorMsg, e);
             throw new IOException(errorMsg, e);
         } catch (Exception e) {
         } catch (Exception e) {
-            // 确保流被关闭
-            cleanupResources(in, out);
-            
             // 如果会话存在且未提交,则放弃会话
             // 如果会话存在且未提交,则放弃会话
             if (currentSession != null) {
             if (currentSession != null) {
                 try {
                 try {
@@ -486,23 +489,58 @@ public class MainActivity extends AppCompatActivity {
     }
     }
 
 
     /**
     /**
-     * 清理文件流资源
+     * 打开 APK 输入流(优先 Uri,其次文件路径)
      */
      */
-    private void cleanupResources(InputStream in, OutputStream out) {
-        if (out != null) {
-            try {
-                out.close();
-            } catch (IOException ignored) {
-                // 忽略关闭异常
+    private InputStream openApkInputStream() throws IOException {
+        if (apkUri != null) {
+            InputStream in = getContentResolver().openInputStream(apkUri);
+            if (in == null) {
+                throw new IOException("无法打开 APK Uri 输入流: " + apkUri);
             }
             }
+            return in;
         }
         }
-        if (in != null) {
-            try {
-                in.close();
-            } catch (IOException ignored) {
-                // 忽略关闭异常
+
+        if (apkPath == null || apkPath.isEmpty()) {
+            throw new IOException("APK 路径为空");
+        }
+
+        File apkFile = new File(apkPath);
+        if (!apkFile.exists()) {
+            throw new IOException("APK 文件不存在: " + apkPath);
+        }
+        return new java.io.FileInputStream(apkFile);
+    }
+
+    /**
+     * 尝试解析 APK 大小(用于 openWrite length & 进度条)
+     * 可能返回 -1(未知)。
+     */
+    private long resolveApkSizeBytes() {
+        try {
+            if (apkUri != null) {
+                Cursor cursor = getContentResolver().query(apkUri, null, null, null, null);
+                if (cursor != null) {
+                    try {
+                        int sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE);
+                        if (sizeIndex >= 0 && cursor.moveToFirst()) {
+                            long size = cursor.getLong(sizeIndex);
+                            return size > 0 ? size : -1;
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                }
+                return -1;
+            }
+
+            if (apkPath != null && !apkPath.isEmpty()) {
+                File apkFile = new File(apkPath);
+                return apkFile.exists() ? apkFile.length() : -1;
             }
             }
+        } catch (Exception ignored) {
+            // 忽略异常,返回未知大小
         }
         }
+        return -1;
     }
     }
 
 
     /**
     /**
@@ -546,15 +584,16 @@ public class MainActivity extends AppCompatActivity {
         
         
         // Log.d(TAG, "onNewIntent 被调用");
         // Log.d(TAG, "onNewIntent 被调用");
         
         
-        // 检查是否是新的安装请求(有 apk_path)
+        // 检查是否是新的安装请求(优先 Intent data 的 Uri;否则 apk_path)
+        Uri newApkUri = intent.getData();
         String newApkPath = intent.getStringExtra(EXTRA_APK_PATH);
         String newApkPath = intent.getStringExtra(EXTRA_APK_PATH);
         if (newApkPath == null || newApkPath.isEmpty()) {
         if (newApkPath == null || newApkPath.isEmpty()) {
             newApkPath = intent.getStringExtra("apk_path");
             newApkPath = intent.getStringExtra("apk_path");
         }
         }
-        
-        if (newApkPath != null && !newApkPath.isEmpty()) {
+
+        if (newApkUri != null || (newApkPath != null && !newApkPath.isEmpty())) {
             // 这是新的安装请求
             // 这是新的安装请求
-            // Log.d(TAG, "检测到新的安装请求: " + newApkPath);
+            // Log.d(TAG, "检测到新的安装请求: " + (newApkUri != null ? newApkUri : newApkPath));
             
             
             if (isInstalling) {
             if (isInstalling) {
                 // 正在安装中,提示用户
                 // 正在安装中,提示用户
@@ -574,6 +613,7 @@ public class MainActivity extends AppCompatActivity {
             // Log.d(TAG, "开始新的安装任务");
             // Log.d(TAG, "开始新的安装任务");
             
             
             // 更新参数
             // 更新参数
+            apkUri = newApkUri;
             apkPath = newApkPath;
             apkPath = newApkPath;
             String newMainPackage = intent.getStringExtra(EXTRA_MAIN_PACKAGE);
             String newMainPackage = intent.getStringExtra(EXTRA_MAIN_PACKAGE);
             if (newMainPackage == null || newMainPackage.isEmpty()) {
             if (newMainPackage == null || newMainPackage.isEmpty()) {

+ 4 - 3
app/build.gradle

@@ -11,8 +11,8 @@ android {
         minSdkVersion 21
         minSdkVersion 21
         targetSdkVersion 32 //建议此属性值设为21 io.dcloud.PandoraEntry 作为apk入口时   必须设置 targetSDKVersion>=21 沉浸式才生效
         targetSdkVersion 32 //建议此属性值设为21 io.dcloud.PandoraEntry 作为apk入口时   必须设置 targetSDKVersion>=21 沉浸式才生效
 
 
-        versionCode 26041501
-        versionName "2.3.43"
+        versionCode 26050901
+        versionName "2.3.50"
         multiDexEnabled true
         multiDexEnabled true
         ndk {
         ndk {
             abiFilters 'x86','x86_64','armeabi', 'armeabi-v7a','arm64-v8a'
             abiFilters 'x86','x86_64','armeabi', 'armeabi-v7a','arm64-v8a'
@@ -164,7 +164,8 @@ android {
         jvmTarget = '1.8'
         jvmTarget = '1.8'
     }
     }
     buildFeatures {
     buildFeatures {
-        compose true
+        buildConfig = true
+        compose = true
     }
     }
     composeOptions {
     composeOptions {
         kotlinCompilerExtensionVersion '1.5.1'
         kotlinCompilerExtensionVersion '1.5.1'

+ 43 - 32
app/src/main/AndroidManifest.xml

@@ -3,21 +3,32 @@
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:tools="http://schemas.android.com/tools"
     package="com.YuyeTech.HeartRate">
     package="com.YuyeTech.HeartRate">
 
 
-    <!-- 自定义权限组声明,放在manifest根节点 -->
+    <!-- 自定义权限组声明,放在manifest根节点
+        android:testOnly="true" -->
     <permission-group
     <permission-group
         android:name="com.YuyeTech.HeartRate.andpermission"
         android:name="com.YuyeTech.HeartRate.andpermission"
         android:label="@string/permission_group_label"
         android:label="@string/permission_group_label"
         android:description="@string/permission_group_desc" />
         android:description="@string/permission_group_desc" />
     <application
     <application
-        android:name="com.YuyeTech.HeartRate.MyApplication"
         android:allowBackup="true"
         android:allowBackup="true"
         android:allowClearUserData="true"
         android:allowClearUserData="true"
         android:icon="@mipmap/ic_launcher"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:label="@string/app_name"
         android:largeHeap="true"
         android:largeHeap="true"
         android:supportsRtl="true"
         android:supportsRtl="true"
-        tools:replace="android:name,android:allowBackup">
+        tools:replace="android:name,android:allowBackup"
+        android:name=".MyApplication">
 
 
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="com.YuyeTech.HeartRate.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/file_paths" />
+        </provider>
         <!-- 应用入口 -->
         <!-- 应用入口 -->
         <activity
         <activity
             android:name="io.dcloud.PandoraEntry"
             android:name="io.dcloud.PandoraEntry"
@@ -104,40 +115,40 @@
         <service
         <service
             android:name="com.baidu.speech.VoiceRecognitionService"
             android:name="com.baidu.speech.VoiceRecognitionService"
             android:exported="false" />
             android:exported="false" />
-        
+
         <!-- ✅ Kiosk Device Admin Receiver(从 myLockView 模块引入) -->
         <!-- ✅ Kiosk Device Admin Receiver(从 myLockView 模块引入) -->
-        <receiver
-            android:name="com.ble.mylockview.admin.KioskDeviceAdminReceiver"
-            android:permission="android.permission.BIND_DEVICE_ADMIN"
-            android:exported="true">
-            
-            <meta-data
-                android:name="android.app.device_admin"
-                android:resource="@xml/device_admin" />
-            
-            <intent-filter>
-                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
-            </intent-filter>
-        </receiver>
-        
+<!--        <receiver-->
+<!--            android:name="com.ble.mylockview.admin.KioskDeviceAdminReceiver"-->
+<!--            android:permission="android.permission.BIND_DEVICE_ADMIN"-->
+<!--            android:exported="true">-->
+
+<!--            <meta-data-->
+<!--                android:name="android.app.device_admin"-->
+<!--                android:resource="@xml/device_admin" />-->
+
+<!--            <intent-filter>-->
+<!--                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />-->
+<!--            </intent-filter>-->
+<!--        </receiver>-->
+
         <!-- ✅ 开机启动接收器(可选,如果需要开机自启动 Kiosk) -->
         <!-- ✅ 开机启动接收器(可选,如果需要开机自启动 Kiosk) -->
-        <receiver
-            android:name="com.ble.mylockview.boot.BootReceiver"
-            android:enabled="true"
-            android:exported="true"
-            android:directBootAware="true">
-            <intent-filter android:priority="1000">
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
-                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
-            </intent-filter>
-        </receiver>
-        
+<!--        <receiver-->
+<!--            android:name="com.ble.mylockview.boot.BootReceiver"-->
+<!--            android:enabled="true"-->
+<!--            android:exported="true"-->
+<!--            android:directBootAware="false">-->
+<!--            <intent-filter android:priority="1000">-->
+<!--                <action android:name="android.intent.action.BOOT_COMPLETED" />-->
+<!--                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />-->
+<!--                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />-->
+<!--            </intent-filter>-->
+<!--        </receiver>-->
+
         <!-- ✅ 屏幕状态广播接收器(监听电源键,实现省电功能) -->
         <!-- ✅ 屏幕状态广播接收器(监听电源键,实现省电功能) -->
         <receiver
         <receiver
-            android:name="com.YuyeTech.HeartRate.ScreenStateReceiver"
             android:enabled="true"
             android:enabled="true"
-            android:exported="false">
+            android:exported="false"
+            android:name=".ScreenStateReceiver">
             <intent-filter android:priority="1000">
             <intent-filter android:priority="1000">
                 <action android:name="android.intent.action.SCREEN_OFF" />
                 <action android:name="android.intent.action.SCREEN_OFF" />
                 <action android:name="android.intent.action.SCREEN_ON" />
                 <action android:name="android.intent.action.SCREEN_ON" />

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/apps/__UNI__8D02B4B/www/app-config-service.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/apps/__UNI__8D02B4B/www/app-service.js


Diff do ficheiro suprimidas por serem muito extensas
+ 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.43","code":26041501},"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.50","code":26050901},"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"}}

+ 15 - 11
app/src/main/java/com/YuyeTech/HeartRate/MyApplication.java

@@ -28,6 +28,7 @@ import com.ble.mylockview.config.LaunchConfig;
 import io.dcloud.PandoraEntry;
 import io.dcloud.PandoraEntry;
 import io.dcloud.PandoraEntryActivity;
 import io.dcloud.PandoraEntryActivity;
 import io.dcloud.application.DCloudApplication;
 import io.dcloud.application.DCloudApplication;
+import io.dcloud.uniplugin.NativeLogBridgeHelper;
 
 
 /**
 /**
  * 自定义 Application,实现 Kiosk 模式自动初始化
  * 自定义 Application,实现 Kiosk 模式自动初始化
@@ -43,6 +44,7 @@ public class MyApplication extends DCloudApplication {
     @Override
     @Override
     public void onCreate() {
     public void onCreate() {
         super.onCreate();
         super.onCreate();
+        NativeLogBridgeHelper.init(this);
         
         
         if (BuildConfig.DEBUG) Log.d(TAG, "Application 初始化");
         if (BuildConfig.DEBUG) Log.d(TAG, "Application 初始化");
 
 
@@ -68,7 +70,7 @@ public class MyApplication extends DCloudApplication {
         LaunchConfig.setLaunchActivity(PandoraEntryActivity.class);
         LaunchConfig.setLaunchActivity(PandoraEntryActivity.class);
         
         
         // ✅ 启用 Debug 模式(发布时改为 false)
         // ✅ 启用 Debug 模式(发布时改为 false)
-        //KioskManager.setDebug(true);
+//        KioskManager.setDebug(true);
         KioskManager.setDebug(BuildConfig.DEBUG);
         KioskManager.setDebug(BuildConfig.DEBUG);
         
         
         // ✅ 注册屏幕状态广播接收器(监听电源键)
         // ✅ 注册屏幕状态广播接收器(监听电源键)
@@ -224,23 +226,25 @@ public class MyApplication extends DCloudApplication {
                 }
                 }
                 
                 
                 // 方法4: 尝试获取输入法服务状态,进一步触发初始化
                 // 方法4: 尝试获取输入法服务状态,进一步触发初始化
-                // 注意:isInputMethodSuppressingSpellChecker() 只在 Android 10+ 存在,所以使用版本检查
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-                    try {
-                        // 通过反射或其他方式检查输入法服务状态
-                        // 这里主要是触发系统初始化输入法服务
-                        imm.isInputMethodSuppressingSpellChecker();
-                    } catch (Exception e) {
-                        // 忽略异常
+                // 注意:该方法在部分 Android 版本/ROM 上可能不存在(会抛 NoSuchMethodError),
+                // 因此这里用反射探测存在性并吞掉所有 Throwable,避免在 Application.onCreate 阶段致命崩溃。
+                try {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                        java.lang.reflect.Method m =
+                                InputMethodManager.class.getMethod("isInputMethodSuppressingSpellChecker");
+                        Object r = m.invoke(imm);
+                        if (BuildConfig.DEBUG) Log.d(TAG, "IME spellChecker suppress: " + r);
                     }
                     }
+                } catch (Throwable t) {
+                    if (BuildConfig.DEBUG) Log.d(TAG, "IME 初始化探测方法不可用,已忽略", t);
                 }
                 }
                 
                 
                 if (BuildConfig.DEBUG) Log.d(TAG, "✅ 输入法服务已初始化(Application onCreate,已检查搜狗输入法)");
                 if (BuildConfig.DEBUG) Log.d(TAG, "✅ 输入法服务已初始化(Application onCreate,已检查搜狗输入法)");
             } else {
             } else {
                 Log.w(TAG, "⚠️ 无法获取InputMethodManager");
                 Log.w(TAG, "⚠️ 无法获取InputMethodManager");
             }
             }
-        } catch (Exception e) {
-            Log.w(TAG, "⚠️ 初始化输入法服务失败", e);
+        } catch (Throwable t) {
+            Log.w(TAG, "⚠️ 初始化输入法服务失败", t);
         }
         }
     }
     }
     
     

+ 132 - 30
app/src/main/java/com/YuyeTech/HeartRate/PowerSavingManager.java

@@ -2,12 +2,14 @@ package com.YuyeTech.HeartRate;
 
 
 import android.app.Activity;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Context;
+import android.os.Build;
 import android.os.PowerManager;
 import android.os.PowerManager;
 import android.util.Log;
 import android.util.Log;
 import android.view.Window;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.WindowManager;
 
 
 import io.dcloud.uniplugin.HeartRateModule;
 import io.dcloud.uniplugin.HeartRateModule;
+import io.dcloud.uniplugin.NativeLogBridgeHelper;
 
 
 import java.lang.ref.WeakReference;
 import java.lang.ref.WeakReference;
 
 
@@ -51,6 +53,7 @@ public class PowerSavingManager {
         }
         }
         
         
         Log.i(TAG, "🔋 开始进入省电模式...");
         Log.i(TAG, "🔋 开始进入省电模式...");
+        NativeLogBridgeHelper.logInfo("NATIVE_POWER", "enterPowerSavingMode");
         isPowerSavingMode = true;
         isPowerSavingMode = true;
         
         
         try {
         try {
@@ -70,6 +73,7 @@ public class PowerSavingManager {
             
             
         } catch (Exception e) {
         } catch (Exception e) {
             Log.e(TAG, "进入省电模式失败", e);
             Log.e(TAG, "进入省电模式失败", e);
+            NativeLogBridgeHelper.logError("NATIVE_POWER", "enterPowerSavingMode failed", e);
         }
         }
     }
     }
     
     
@@ -84,14 +88,17 @@ public class PowerSavingManager {
         }
         }
         
         
         Log.i(TAG, "🔋 开始退出省电模式...");
         Log.i(TAG, "🔋 开始退出省电模式...");
+        NativeLogBridgeHelper.logInfo("NATIVE_POWER", "exitPowerSavingMode");
         isPowerSavingMode = false;
         isPowerSavingMode = false;
         
         
         try {
         try {
             // 1. 恢复屏幕亮度
             // 1. 恢复屏幕亮度
-            //restoreScreenBrightness();
+            restoreScreenBrightness();
             
             
             // 2. 恢复 UI 刷新(重新设置 FLAG_KEEP_SCREEN_ON)
             // 2. 恢复 UI 刷新(重新设置 FLAG_KEEP_SCREEN_ON)
-            //resumeUIRefresh();
+            // 原逻辑曾因临时方案而注释掉:resumeUIRefresh();
+            // 为避免 Android 11 亮屏瞬间 Window 修改失效,这里恢复调用;内部会做 UI 线程切换与兼容处理。
+            resumeUIRefresh();
             
             
             // 3. 恢复外设(蓝牙扫描等)
             // 3. 恢复外设(蓝牙扫描等)
             resumePeripherals(context);
             resumePeripherals(context);
@@ -100,6 +107,7 @@ public class PowerSavingManager {
             
             
         } catch (Exception e) {
         } catch (Exception e) {
             Log.e(TAG, "退出省电模式失败", e);
             Log.e(TAG, "退出省电模式失败", e);
+            NativeLogBridgeHelper.logError("NATIVE_POWER", "exitPowerSavingMode failed", e);
         }
         }
     }
     }
     
     
@@ -162,31 +170,47 @@ public class PowerSavingManager {
      * 恢复屏幕亮度
      * 恢复屏幕亮度
      */
      */
     private static void restoreScreenBrightness() {
     private static void restoreScreenBrightness() {
+        // Android 11+:Window 属性修改必须在 UI 线程,否则可能无效或抛异常。
+        // 同时按需求:不影响系统休眠策略,仅对当前 App Window 强制提亮到 100%。
         try {
         try {
-            Activity activity = getCurrentActivity();
+            final Activity activity = getCurrentActivity();
             if (activity == null) {
             if (activity == null) {
                 Log.w(TAG, "Activity 不可用,无法恢复亮度");
                 Log.w(TAG, "Activity 不可用,无法恢复亮度");
                 return;
                 return;
             }
             }
-            
-            Window window = activity.getWindow();
-            WindowManager.LayoutParams params = window.getAttributes();
-            
-            // 恢复保存的亮度(如果之前保存过)
-            if (savedScreenBrightness >= 0) {
-                params.screenBrightness = savedScreenBrightness;
-            } else {
-                // 如果没有保存,使用系统默认(-1)
-                params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
-            }
-            
-            window.setAttributes(params);
+
+            // 必须在 UI 线程修改 Window 属性
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        Window window = activity.getWindow();
+                        WindowManager.LayoutParams params = window.getAttributes();
+
+                        // 旧逻辑(保留不删):按 savedScreenBrightness 恢复或用系统默认
+                        // if (savedScreenBrightness >= 0) {
+                        //     params.screenBrightness = savedScreenBrightness;
+                        // } else {
+                        //     params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
+                        // }
+
+                        // 新逻辑:强制 100%(仅对当前 App Window 生效,不改系统设置)
+                        params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL; // = 1.0f
+                        window.setAttributes(params);
+
+                        // 建议同时保持常亮,防止提亮后又瞬间熄屏(是否真正休眠仍由系统策略/后续逻辑决定)
+                        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+                        Log.d(TAG, "✅ 屏幕亮度已通过 Window 强制设为 100%");
+                    } catch (Exception e) {
+                        Log.e(TAG, "UI线程内设置屏幕亮度失败", e);
+                    }
+                }
+            });
+
             savedScreenBrightness = -1; // 重置
             savedScreenBrightness = -1; // 重置
-            
-            Log.d(TAG, "✅ 屏幕亮度已恢复");
-            
         } catch (Exception e) {
         } catch (Exception e) {
-            Log.e(TAG, "恢复屏幕亮度失败", e);
+            Log.e(TAG, "恢复屏幕亮度方法执行失败", e);
         }
         }
     }
     }
     
     
@@ -218,21 +242,33 @@ public class PowerSavingManager {
      * 临时方案下(allowKeepScreenOn=false)不恢复常亮,由系统休眠时间控制息屏
      * 临时方案下(allowKeepScreenOn=false)不恢复常亮,由系统休眠时间控制息屏
      */
      */
     private static void resumeUIRefresh() {
     private static void resumeUIRefresh() {
-        if (!allowKeepScreenOn) {
-            Log.d(TAG, "临时方案:不恢复 FLAG_KEEP_SCREEN_ON,跟随系统休眠");
-            return;
-        }
+        // 兼容要求:退出省电模式时,强制恢复常亮标志(UI线程),提升亮度后避免立即熄屏。
+        // 注意:保留 allowKeepScreenOn 的旧分支逻辑(不删除),冲突处注释标明。
+
+        // 旧逻辑(保留不删):allowKeepScreenOn=false 时不恢复常亮,跟随系统休眠
+        // if (!allowKeepScreenOn) {
+        //     Log.d(TAG, "临时方案:不恢复 FLAG_KEEP_SCREEN_ON,跟随系统休眠");
+        //     return;
+        // }
+
         try {
         try {
-            Activity activity = getCurrentActivity();
+            final Activity activity = getCurrentActivity();
             if (activity == null) {
             if (activity == null) {
                 Log.w(TAG, "Activity 不可用,无法恢复 UI 刷新");
                 Log.w(TAG, "Activity 不可用,无法恢复 UI 刷新");
                 return;
                 return;
             }
             }
-            
-            Window window = activity.getWindow();
-            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-            Log.d(TAG, "✅ UI 刷新已恢复(已重新设置 FLAG_KEEP_SCREEN_ON)");
-            
+
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+                        Log.d(TAG, "✅ 已强制设置 FLAG_KEEP_SCREEN_ON");
+                    } catch (Exception e) {
+                        Log.e(TAG, "恢复常亮失败", e);
+                    }
+                }
+            });
         } catch (Exception e) {
         } catch (Exception e) {
             Log.e(TAG, "恢复 UI 刷新失败", e);
             Log.e(TAG, "恢复 UI 刷新失败", e);
         }
         }
@@ -273,6 +309,62 @@ public class PowerSavingManager {
         }
         }
         return null;
         return null;
     }
     }
+
+    /**
+     * Android 是否处于可交互态(亮屏可用)
+     * 对齐前端 sleepScreenUtil.js 的 PowerManager.isInteractive 语义
+     */
+    private static boolean isAndroidScreenInteractive(Activity activity) {
+        try {
+            if (activity == null) return false;
+            PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
+            if (pm == null) return false;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
+                return pm.isInteractive();
+            }
+            // 老版本兜底
+            return pm.isScreenOn();
+        } catch (Exception e) {
+            Log.e(TAG, "isAndroidScreenInteractive 判断失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * App 启动/页面显示时:可交互才提亮到 100%,仅影响当前 Window,不改系统设置。
+     * 不额外加常亮标志,避免影响休眠策略。
+     */
+    private static void boostBrightnessIfInteractiveOnStart(final Activity activity) {
+        try {
+            if (activity == null) return;
+            if (!isAndroidScreenInteractive(activity)) {
+                Log.d(TAG, "启动提亮跳过:当前不可交互");
+                return;
+            }
+            activity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        if (activity.isFinishing()) return;
+                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) return;
+
+                        Window window = activity.getWindow();
+                        WindowManager.LayoutParams params = window.getAttributes();
+
+                        // 仅对当前 Window 强制 100% 亮度;不加 FLAG_KEEP_SCREEN_ON(不影响休眠)
+                        params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL;
+                        window.setAttributes(params);
+
+                        Log.d(TAG, "✅ 启动/页面显示:可交互态下已强制设为 100% 亮度");
+                    } catch (Exception e) {
+                        Log.e(TAG, "启动提亮(UI线程)失败", e);
+                    }
+                }
+            });
+        } catch (Exception e) {
+            Log.e(TAG, "启动提亮方法执行失败", e);
+        }
+    }
     
     
     /**
     /**
      * 设置当前 Activity(由 MyApplication 调用)
      * 设置当前 Activity(由 MyApplication 调用)
@@ -280,6 +372,16 @@ public class PowerSavingManager {
     public static void setCurrentActivity(Activity activity) {
     public static void setCurrentActivity(Activity activity) {
         if (activity != null) {
         if (activity != null) {
             currentActivityRef = new WeakReference<>(activity);
             currentActivityRef = new WeakReference<>(activity);
+            // 启动/页面切换时也需要提亮:仅当屏幕可交互且不在省电模式时执行
+            try {
+                if (!isPowerSavingMode) {
+                    boostBrightnessIfInteractiveOnStart(activity);
+                } else {
+                    Log.d(TAG, "省电模式中:跳过启动提亮");
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "setCurrentActivity 内启动提亮失败", e);
+            }
         } else {
         } else {
             currentActivityRef = null;
             currentActivityRef = null;
         }
         }

+ 5 - 0
app/src/main/res/xml/file_paths.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+    <!-- 对应 /storage/emulated/0/Android/data/com.YuyeTech.HeartRate/ -->
+    <external-path name="my_apps" path="Android/data/com.YuyeTech.HeartRate/" />
+</paths>

+ 24 - 17
myLockView/src/main/AndroidManifest.xml

@@ -9,37 +9,44 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <application>
     <application>
 
 
-        <!-- 开机启动广播接收器 -->
-        <receiver
-            android:name="com.ble.mylockview.boot.BootReceiver"
-            android:enabled="true"
-            android:exported="true"
-            android:directBootAware="true">
-
-            <intent-filter android:priority="1000">
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
-                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
-            </intent-filter>
-
-        </receiver>
-
         <!-- Device Owner / Kiosk 设备管理员 -->
         <!-- Device Owner / Kiosk 设备管理员 -->
+        <!-- 1. 核心管理员接收器:保持纯净,只处理系统广播 -->
         <receiver
         <receiver
             android:name="com.ble.mylockview.admin.KioskDeviceAdminReceiver"
             android:name="com.ble.mylockview.admin.KioskDeviceAdminReceiver"
             android:permission="android.permission.BIND_DEVICE_ADMIN"
             android:permission="android.permission.BIND_DEVICE_ADMIN"
             android:exported="true">
             android:exported="true">
-
             <meta-data
             <meta-data
                 android:name="android.app.device_admin"
                 android:name="android.app.device_admin"
                 android:resource="@xml/device_admin" />
                 android:resource="@xml/device_admin" />
-
             <intent-filter>
             <intent-filter>
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
             </intent-filter>
             </intent-filter>
 
 
         </receiver>
         </receiver>
 
 
+        <!-- 2. 清除权限的“后门”接收器:不要加 BIND_DEVICE_ADMIN 权限,否则 ADB 广播进不来 -->
+        <receiver
+            android:name="com.ble.mylockview.admin.ClearAdminReceiver"
+            android:exported="true">
+            <intent-filter>
+<!--                <action android:name="com.YuyeTech.ACTION_CLEAR_DEVICE_OWNER" />-->
+                <action android:name="com.YuyeTech.HEART_RATE_MASTER_CONTROL_OFF_9527X" />
+            </intent-filter>
+        </receiver>
+
+        <!-- 开机启动广播接收器 -->
+        <!-- 3. 开机启动接收器:修复崩溃,关闭 directBootAware -->
+        <receiver
+            android:name="com.ble.mylockview.boot.BootReceiver"
+            android:enabled="true"
+            android:exported="true"
+            android:directBootAware="false">
+            <intent-filter android:priority="1000">
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
+            </intent-filter>
+        </receiver>
         <!-- 启动服务 -->
         <!-- 启动服务 -->
         <service
         <service
             android:name="com.ble.mylockview.service.StartService"
             android:name="com.ble.mylockview.service.StartService"

+ 36 - 0
myLockView/src/main/java/com/ble/mylockview/admin/ClearAdminReceiver.java

@@ -0,0 +1,36 @@
+package com.ble.mylockview.admin;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class ClearAdminReceiver extends BroadcastReceiver {
+
+    // 1. 复杂的 Action
+    private static final String SECRET_ACTION = "com.YuyeTech.HEART_RATE_MASTER_CONTROL_OFF_9527X";
+    // 2. 只有你才知道的口令键值对
+    private static final String SECRET_KEY = "auth_token";
+    private static final String SECRET_VALUE = "Yuye@2024_Admin_Exit";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        // 验证 Action
+        if (SECRET_ACTION.equals(intent.getAction())) {
+
+            // 验证 Extra 口令
+            String token = intent.getStringExtra(SECRET_KEY);
+            if (!SECRET_VALUE.equals(token)) {
+                Log.w("KioskAdmin", "非法尝试触发清除逻辑,口令错误!");
+                return;
+            }
+
+            DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+            if (dpm.isDeviceOwnerApp(context.getPackageName())) {
+                dpm.clearDeviceOwnerApp(context.getPackageName());
+                Log.d("KioskAdmin", "!!!Device Owner 权限已通过后门成功清除!!!");
+            }
+        }
+    }
+}

+ 11 - 10
myWIFIView/src/main/java/com/ble/mywifiview/WifiModule.java

@@ -244,23 +244,24 @@ public class WifiModule {
                 }
                 }
                 
                 
                 // 方法4: 尝试获取输入法服务状态,进一步触发初始化
                 // 方法4: 尝试获取输入法服务状态,进一步触发初始化
-                // 注意:isInputMethodSuppressingSpellChecker() 只在 Android 10+ 存在,所以使用版本检查
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-                    try {
-                        // 通过反射或其他方式检查输入法服务状态
-                        // 这里主要是触发系统初始化输入法服务
-                        imm.isInputMethodSuppressingSpellChecker();
-                    } catch (Exception e) {
-                        // 忽略异常
+                // 注意:该方法在部分 Android 版本/ROM 上可能不存在(会抛 NoSuchMethodError),
+                // 因此这里用反射探测存在性并吞掉所有 Throwable,避免崩溃。
+                try {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                        java.lang.reflect.Method m =
+                                InputMethodManager.class.getMethod("isInputMethodSuppressingSpellChecker");
+                        m.invoke(imm);
                     }
                     }
+                } catch (Throwable ignore) {
+                    // ignore
                 }
                 }
                 
                 
                 Log.d(TAG, "✅ 输入法服务已初始化(多种方式,已检查搜狗输入法)");
                 Log.d(TAG, "✅ 输入法服务已初始化(多种方式,已检查搜狗输入法)");
             } else {
             } else {
                 Log.w(TAG, "无法获取InputMethodManager");
                 Log.w(TAG, "无法获取InputMethodManager");
             }
             }
-        } catch (Exception e) {
-            Log.w(TAG, "初始化输入法服务失败(不影响WiFi设置打开)", e);
+        } catch (Throwable t) {
+            Log.w(TAG, "初始化输入法服务失败(不影响WiFi设置打开)", t);
             // 即使初始化失败,也不影响打开WiFi设置
             // 即使初始化失败,也不影响打开WiFi设置
         }
         }
     }
     }

+ 88 - 0
uniplugin_module/src/main/java/io/dcloud/uniplugin/AppLog.java

@@ -0,0 +1,88 @@
+package io.dcloud.uniplugin;
+
+import android.util.Log;
+
+import com.alibaba.fastjson.JSONObject;
+
+/**
+ * 统一原生日志出口:
+ * - 仍写入 logcat(方便 Android Studio 调试)
+ * - 同时写入 NativeLogBridgeHelper(落盘 + 可桥接到 JS + feedback 可拉取)
+ *
+ * 使用建议:
+ * - 关键链路/可追溯问题:用 AppLog.*
+ * - 临时本地调试:仍可用 Log.*(不会进入落盘/桥接)
+ */
+public final class AppLog {
+
+    private AppLog() {}
+
+    /**
+     * 控制是否写入 NativeLogBridgeHelper(落盘/桥接)。
+     * - true:logcat + 落盘/桥接
+     * - false:仅 logcat(不会写入 native-client-logs,也不会 fire NativeLogBridge)
+     */
+    private static volatile boolean bridgeEnabled = true;
+
+    public static void setBridgeEnabled(boolean enabled) {
+        bridgeEnabled = enabled;
+    }
+
+    public static boolean isBridgeEnabled() {
+        return bridgeEnabled;
+    }
+
+    public static void d(String tag, String msg) {
+        try { Log.d(tag, msg); } catch (Exception ignore) {}
+        if (!bridgeEnabled) return;
+        try { NativeLogBridgeHelper.log("INFO", safeTag(tag), msg, null, null, true); } catch (Exception ignore) {}
+    }
+
+    public static void i(String tag, String msg) {
+        try { Log.i(tag, msg); } catch (Exception ignore) {}
+        if (!bridgeEnabled) return;
+        try { NativeLogBridgeHelper.log("INFO", safeTag(tag), msg, null, null, true); } catch (Exception ignore) {}
+    }
+
+    public static void w(String tag, String msg) {
+        try { Log.w(tag, msg); } catch (Exception ignore) {}
+        if (!bridgeEnabled) return;
+        try { NativeLogBridgeHelper.log("WARN", safeTag(tag), msg, null, null, true); } catch (Exception ignore) {}
+    }
+
+    public static void e(String tag, String msg) {
+        try { Log.e(tag, msg); } catch (Exception ignore) {}
+        if (!bridgeEnabled) return;
+        try { NativeLogBridgeHelper.log("ERROR", safeTag(tag), msg, null, null, true); } catch (Exception ignore) {}
+    }
+
+    public static void e(String tag, String msg, Throwable tr) {
+        try { Log.e(tag, msg, tr); } catch (Exception ignore) {}
+        if (!bridgeEnabled) return;
+        try { NativeLogBridgeHelper.log("ERROR", safeTag(tag), msg, tr, null, true); } catch (Exception ignore) {}
+    }
+
+    public static void json(String level, String tag, String msg, JSONObject extra) {
+        String lvl = level == null ? "INFO" : level;
+        try {
+            if ("ERROR".equalsIgnoreCase(lvl)) {
+                Log.e(tag, msg);
+            } else if ("WARN".equalsIgnoreCase(lvl)) {
+                Log.w(tag, msg);
+            } else if ("DEBUG".equalsIgnoreCase(lvl)) {
+                Log.d(tag, msg);
+            } else {
+                Log.i(tag, msg);
+            }
+        } catch (Exception ignore) {}
+        if (!bridgeEnabled) return;
+        try { NativeLogBridgeHelper.log(lvl, safeTag(tag), msg, null, extra, true); } catch (Exception ignore) {}
+    }
+
+    private static String safeTag(String tag) {
+        if (tag == null || tag.trim().isEmpty()) return "NATIVE";
+        String t = tag.trim();
+        return t.length() > 40 ? t.substring(0, 40) : t;
+    }
+}
+

+ 93 - 11
uniplugin_module/src/main/java/io/dcloud/uniplugin/HeartRateModule.java

@@ -9,6 +9,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager;
+import android.location.LocationManager;
 import android.net.Uri;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Handler;
@@ -94,6 +95,17 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
         sInstance = this;
         sInstance = this;
     }
     }
 
 
+    static void emitNativeLog(JSONObject payload) {
+        if (payload == null || sInstance == null || sInstance.mUniSDKInstance == null) {
+            return;
+        }
+        try {
+            sInstance.mUniSDKInstance.fireGlobalEventCallback("NativeLogBridge", payload);
+        } catch (Exception e) {
+            Log.e("HeartRateModule", "emitNativeLog error", e);
+        }
+    }
+
 
 
     /**
     /**
      * 百度语音
      * 百度语音
@@ -118,6 +130,37 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
         return data;
         return data;
     }
     }
 
 
+    @UniJSMethod(uiThread = false)
+    public JSONObject SLA_GetNativeFeedbackBundle() {
+        return NativeLogBridgeHelper.getFeedbackBundle();
+    }
+
+    @UniJSMethod(uiThread = false)
+    public JSONObject SLA_GetNativeCrashInfo() {
+        return NativeLogBridgeHelper.getCrashInfo();
+    }
+
+    @UniJSMethod(uiThread = false)
+    public JSONObject SLA_ClearNativeCrashInfo() {
+        NativeLogBridgeHelper.clearCrashInfo();
+        JSONObject data = new JSONObject();
+        data.put("code", 0);
+        data.put("msg", "clear native crash info success");
+        return data;
+    }
+
+    @UniJSMethod(uiThread = false)
+    public JSONObject SLA_RecordNativeLog(JSONObject options) {
+        String level = options == null ? "INFO" : options.getString("level");
+        String tag = options == null ? "NATIVE_MANUAL" : options.getString("tag");
+        String msg = options == null ? "" : options.getString("msg");
+        NativeLogBridgeHelper.log(level, tag, msg, null, options, true);
+        JSONObject data = new JSONObject();
+        data.put("code", 0);
+        data.put("msg", "record native log success");
+        return data;
+    }
+
     /**
     /**
      * JS 用于查询“是否存在待处理的省电恢复事件”(powerSavingPeripheralsResumed)。
      * JS 用于查询“是否存在待处理的省电恢复事件”(powerSavingPeripheralsResumed)。
      * 典型场景:App 被重启后,ScreenStateReceiver 已经在原生侧收到 ACTION_SCREEN_ON 并调用了 resumeForPowerSaving,
      * 典型场景:App 被重启后,ScreenStateReceiver 已经在原生侧收到 ACTION_SCREEN_ON 并调用了 resumeForPowerSaving,
@@ -380,6 +423,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
         if (sInstance == null) return;
         if (sInstance == null) return;
         try {
         try {
             if (sInstance.mUniSDKInstance == null) return;
             if (sInstance.mUniSDKInstance == null) return;
+            AppLog.i("NATIVE_POWER", "powerSavingPeripheralsStopped");
             JSONObject data = new JSONObject();
             JSONObject data = new JSONObject();
             data.put("type", "powerSavingPeripheralsStopped");
             data.put("type", "powerSavingPeripheralsStopped");
             data.put("reason", "screenOff");
             data.put("reason", "screenOff");
@@ -387,6 +431,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
             Log.d("HeartRateModule", "fireGlobalEventCallback: powerSavingPeripheralsStopped");
             Log.d("HeartRateModule", "fireGlobalEventCallback: powerSavingPeripheralsStopped");
         } catch (Exception e) {
         } catch (Exception e) {
             Log.e("HeartRateModule", "fire powerSavingPeripheralsStopped error", e);
             Log.e("HeartRateModule", "fire powerSavingPeripheralsStopped error", e);
+            AppLog.e("NATIVE_POWER", "fire powerSavingPeripheralsStopped error", e);
         }
         }
     }
     }
 
 
@@ -403,6 +448,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
             return;
             return;
         }
         }
         try {
         try {
+            AppLog.i("NATIVE_POWER", "powerSavingPeripheralsResumed");
             JSONObject data = new JSONObject();
             JSONObject data = new JSONObject();
             data.put("type", "powerSavingPeripheralsResumed");
             data.put("type", "powerSavingPeripheralsResumed");
             data.put("reason", "screenOn");
             data.put("reason", "screenOn");
@@ -410,6 +456,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
             Log.d("HeartRateModule", "fireGlobalEventCallback: powerSavingPeripheralsResumed");
             Log.d("HeartRateModule", "fireGlobalEventCallback: powerSavingPeripheralsResumed");
         } catch (Exception e) {
         } catch (Exception e) {
             Log.e("HeartRateModule", "fire powerSavingPeripheralsResumed error", e);
             Log.e("HeartRateModule", "fire powerSavingPeripheralsResumed error", e);
+            AppLog.e("NATIVE_POWER", "fire powerSavingPeripheralsResumed error", e);
         }
         }
     }
     }
 
 
@@ -606,24 +653,52 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
 //            return data;
 //            return data;
 //        }
 //        }
 
 
-        if (!checkBlePermission(mUniSDKInstance.getContext())) {
-
-            requestBlePermission((Activity) mUniSDKInstance.getContext());
+        if (!isBLEEnabled()) {
+            AppLog.w("NATIVE_BLE", "HeartBLE_StartScan bluetooth disabled");
+            showBLEDialog();
             JSONObject data = new JSONObject();
             JSONObject data = new JSONObject();
-            data.put("code", 400);
+            data.put("code", 401);
             data.put("bPermission", false);
             data.put("bPermission", false);
             return data;
             return data;
         }
         }
 
 
-        if (!isBLEEnabled()) {
-            showBLEDialog();
+        // Android 6 ~ 11:即使已授予定位权限,若系统“定位服务开关”关闭,BLE 扫描结果也可能一直为空
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+            try {
+                LocationManager lm = (LocationManager) mUniSDKInstance.getContext().getSystemService(Context.LOCATION_SERVICE);
+                boolean enabled = lm != null && (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)
+                        || lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER));
+                if (!enabled) {
+                    AppLog.w("NATIVE_BLE", "HeartBLE_StartScan location service disabled (Android < 12)");
+                    myToastNormal("请先打开系统定位服务,否则可能扫不到设备");
+                    Intent i = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+                    Context ctx = mUniSDKInstance.getContext();
+                    if (!(ctx instanceof Activity)) {
+                        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    }
+                    ctx.startActivity(i);
+                    JSONObject data = new JSONObject();
+                    data.put("code", 403);
+                    data.put("bPermission", true);
+                    data.put("bLocationEnabled", false);
+                    return data;
+                }
+            } catch (Throwable t) {
+                AppLog.w("NATIVE_BLE", "HeartBLE_StartScan location enabled check failed: " + t);
+            }
+        }
+
+        if (!checkBlePermission(mUniSDKInstance.getContext())) {
+            AppLog.w("NATIVE_BLE", "HeartBLE_StartScan permission missing");
+            requestBlePermission((Activity) mUniSDKInstance.getContext());
             JSONObject data = new JSONObject();
             JSONObject data = new JSONObject();
-            data.put("code", 401);
+            data.put("code", 400);
             data.put("bPermission", false);
             data.put("bPermission", false);
             return data;
             return data;
         }
         }
         if (mIsScanning) {
         if (mIsScanning) {
             //正在连接中,不能重复扫
             //正在连接中,不能重复扫
+            AppLog.w("NATIVE_BLE", "HeartBLE_StartScan ignored because already scanning");
             JSONObject data = new JSONObject();
             JSONObject data = new JSONObject();
             data.put("code", 402);
             data.put("code", 402);
             data.put("bPermission", true);
             data.put("bPermission", true);
@@ -633,6 +708,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
         //mManager.setFilterNames(FILTER_NAMES);
         //mManager.setFilterNames(FILTER_NAMES);
         //mManager.startScan(mScanCallback);
         //mManager.startScan(mScanCallback);
         heartRateSdkManager.startBluetoothOperations();
         heartRateSdkManager.startBluetoothOperations();
+        AppLog.i("NATIVE_BLE", "HeartBLE_StartScan started");
         mIsScanning = true;
         mIsScanning = true;
         // 若启用下方 Toast:Uniapp 需在 onDeviceConnected/onStopScan 时调用 uni.hideToast(),否则会蒙层 15 秒挡住按钮;建议 duration 用较短值(如 2000)且前端 mask 慎重
         // 若启用下方 Toast:Uniapp 需在 onDeviceConnected/onStopScan 时调用 uni.hideToast(),否则会蒙层 15 秒挡住按钮;建议 duration 用较短值(如 2000)且前端 mask 慎重
         myToast("正在连接", SCAN_DURATION);
         myToast("正在连接", SCAN_DURATION);
@@ -663,11 +739,12 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
 
 
     protected void showBLEDialog() {
     protected void showBLEDialog() {
         final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
         final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
-        if (checkBlePermission(mUniSDKInstance.getContext())) {
-            mUniSDKInstance.getContext().startActivity(enableIntent);
-        } else {
-            requestBlePermission((Activity) mUniSDKInstance.getContext());
+        // 开启蓝牙不依赖定位/蓝牙扫描权限;避免因权限未授予导致无法弹出“开启蓝牙”系统对话框
+        Context ctx = mUniSDKInstance.getContext();
+        if (!(ctx instanceof Activity)) {
+            enableIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         }
         }
+        ctx.startActivity(enableIntent);
     }
     }
 
 
     /**
     /**
@@ -816,6 +893,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
     @Override
     @Override
     public void onDeviceConnected(@NonNull BluetoothDevice device) {
     public void onDeviceConnected(@NonNull BluetoothDevice device) {
         Log.i(TAG, "==onDeviceConnected: "+device);
         Log.i(TAG, "==onDeviceConnected: "+device);
+        AppLog.i("NATIVE_BLE", "device connected");
         mHandler.removeCallbacks(scanRunnable); // 取消延迟任务
         mHandler.removeCallbacks(scanRunnable); // 取消延迟任务
         HeartBLE_StopScan();
         HeartBLE_StopScan();
         mDeviceConnected = true;
         mDeviceConnected = true;
@@ -835,6 +913,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
     @Override
     @Override
     public void onDeviceDisconnected(@NonNull BluetoothDevice device) {
     public void onDeviceDisconnected(@NonNull BluetoothDevice device) {
         Log.i(TAG, "==onDeviceDisconnected: "+device);
         Log.i(TAG, "==onDeviceDisconnected: "+device);
+        AppLog.w("NATIVE_BLE", "device disconnected");
         mDeviceConnected = false;
         mDeviceConnected = false;
 //        myToastNormal("连接断开");
 //        myToastNormal("连接断开");
         //mManager.close();
         //mManager.close();
@@ -959,6 +1038,7 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
                 data.put("requestCodeStr","RecordAudioRequestCode");
                 data.put("requestCodeStr","RecordAudioRequestCode");
                 data.put("msg","录音权限");
                 data.put("msg","录音权限");
                 if (granted) {
                 if (granted) {
+                    AppLog.i("NATIVE_PERMISSION", "record audio permission granted");
                     data.put("status", "onGranted"); // 允许
                     data.put("status", "onGranted"); // 允许
                 } else {
                 } else {
                     // 判断是否是“拒绝并不再询问”
                     // 判断是否是“拒绝并不再询问”
@@ -967,8 +1047,10 @@ public class HeartRateModule extends UniModule implements HearRateManagerCallbac
                             Manifest.permission.RECORD_AUDIO
                             Manifest.permission.RECORD_AUDIO
                     );
                     );
                     if (dontAskAgain) {
                     if (dontAskAgain) {
+                        AppLog.w("NATIVE_PERMISSION", "record audio permission never ask again");
                         data.put("status", "onNeverAskAgain"); // 永久拒绝
                         data.put("status", "onNeverAskAgain"); // 永久拒绝
                     } else {
                     } else {
+                        AppLog.w("NATIVE_PERMISSION", "record audio permission denied");
                         data.put("status", "onDenied"); // 临时拒绝
                         data.put("status", "onDenied"); // 临时拒绝
                     }
                     }
                 }
                 }

+ 22 - 4
uniplugin_module/src/main/java/io/dcloud/uniplugin/InstallModule.java

@@ -4,11 +4,14 @@ import android.app.Activity;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Context;
+import android.content.ClipData;
 import android.content.Intent;
 import android.content.Intent;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller;
 import android.os.Build;
 import android.os.Build;
 import android.util.Log;
 import android.util.Log;
 
 
+import androidx.core.content.FileProvider;
+
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
 
 
 import java.io.File;
 import java.io.File;
@@ -17,6 +20,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStream;
 
 
+import android.net.Uri;
+
 import io.dcloud.feature.uniapp.annotation.UniJSMethod;
 import io.dcloud.feature.uniapp.annotation.UniJSMethod;
 import io.dcloud.feature.uniapp.bridge.UniJSCallback;
 import io.dcloud.feature.uniapp.bridge.UniJSCallback;
 import io.dcloud.feature.uniapp.common.UniModule;
 import io.dcloud.feature.uniapp.common.UniModule;
@@ -75,6 +80,17 @@ public class InstallModule extends UniModule {
                 apkPath = apkPath.substring("file://".length());
                 apkPath = apkPath.substring("file://".length());
             }
             }
 
 
+            File apkFile = new File(apkPath);
+            if (!apkFile.exists()) {
+                throw new IOException("APK 文件不存在: " + apkPath);
+            }
+
+            Uri apkUri = FileProvider.getUriForFile(
+                    context,
+                    "com.YuyeTech.HeartRate.fileprovider",
+                    apkFile
+            );
+
             // 获取当前 Activity(用于退出 Kiosk)
             // 获取当前 Activity(用于退出 Kiosk)
             Activity activity = null;
             Activity activity = null;
             if (context instanceof Activity) {
             if (context instanceof Activity) {
@@ -105,11 +121,13 @@ public class InstallModule extends UniModule {
             addUpdateHelperToLockTaskList(context);
             addUpdateHelperToLockTaskList(context);
 
 
             // 启动 UpdateHelper
             // 启动 UpdateHelper
-            Intent intent = new Intent("com.yuyetech.updatehelper.INSTALL_APK");
-            intent.setPackage("com.yuyetech.updatehelper");
-            intent.putExtra("apk_path", apkPath);
+            Intent intent = new Intent();
+            intent.setClassName("com.yuyetech.updatehelper", "com.yuyetech.updatehelper.MainActivity");
+            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
+            intent.setClipData(ClipData.newUri(context.getContentResolver(), "apk", apkUri));
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
             intent.putExtra("main_package", context.getPackageName());
             intent.putExtra("main_package", context.getPackageName());
-            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
             
             
             try {
             try {
                 context.startActivity(intent);
                 context.startActivity(intent);

+ 295 - 0
uniplugin_module/src/main/java/io/dcloud/uniplugin/NativeLogBridgeHelper.java

@@ -0,0 +1,295 @@
+package io.dcloud.uniplugin;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.alibaba.fastjson.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * 原生日志落地与 JS 桥接帮助类。
+ * 目标:
+ * 1. 原生异常/关键日志本地保留 2~3 天;
+ * 2. JS 侧可主动拉取反馈所需的 native 日志摘要;
+ * 3. 当 JS 已就绪时,关键日志可通过 NativeLogBridge 实时透传给前端日志链路。
+ */
+public final class NativeLogBridgeHelper {
+
+    private static final String TAG = "NativeLogBridgeHelper";
+    private static final String PREF_NAME = "native_log_bridge";
+    private static final String KEY_LAST_CRASH_HINT = "last_crash_hint";
+    private static final String KEY_LAST_CRASH_AT = "last_crash_at";
+    private static final String KEY_LAST_CRASH_STACK = "last_crash_stack";
+    private static final long RETENTION_MS = 3L * 24 * 60 * 60 * 1000;
+    private static final int MAX_EXPORT_CHARS = 12000;
+    private static final int MAX_STACK_CHARS = 4000;
+    private static final String NATIVE_LOG_EVENT = "NativeLogBridge";
+
+    private static volatile Context appContext;
+    private static volatile Thread.UncaughtExceptionHandler previousHandler;
+    private static volatile boolean initialized = false;
+
+    private NativeLogBridgeHelper() {
+    }
+
+    public static synchronized void init(Context context) {
+        if (context == null) {
+            return;
+        }
+        appContext = context.getApplicationContext();
+        cleanupExpiredFiles();
+        if (initialized) {
+            return;
+        }
+        previousHandler = Thread.getDefaultUncaughtExceptionHandler();
+        Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
+            try {
+                recordCrash("NATIVE_CRASH", throwable);
+            } catch (Exception e) {
+                Log.e(TAG, "recordCrash failed", e);
+            }
+            if (previousHandler != null) {
+                previousHandler.uncaughtException(thread, throwable);
+            }
+        });
+        initialized = true;
+        log("INFO", "NATIVE_BOOT", "native log bridge initialized", null, null, false);
+    }
+
+    public static void logInfo(String tag, String msg) {
+        log("INFO", tag, msg, null, null, true);
+    }
+
+    public static void logWarn(String tag, String msg) {
+        log("WARN", tag, msg, null, null, true);
+    }
+
+    public static void logError(String tag, String msg, Throwable throwable) {
+        log("ERROR", tag, msg, throwable, null, true);
+    }
+
+    public static void log(String level, String tag, String msg, Throwable throwable, JSONObject extra, boolean emitToJs) {
+        Context context = appContext;
+        if (context == null) {
+            Log.w(TAG, "log ignored because appContext is null");
+            return;
+        }
+        cleanupExpiredFiles();
+        JSONObject line = new JSONObject();
+        long ts = System.currentTimeMillis();
+        line.put("ts", ts);
+        line.put("level", safeLevel(level));
+        line.put("tag", safeTag(tag));
+        line.put("msg", safeText(msg, 800));
+        if (throwable != null) {
+            line.put("stack", safeText(Log.getStackTraceString(throwable), MAX_STACK_CHARS));
+        }
+        if (extra != null && !extra.isEmpty()) {
+            line.put("extra", extra);
+        }
+        appendLine(context, line);
+        if (emitToJs) {
+            HeartRateModule.emitNativeLog(line);
+        }
+    }
+
+    public static void recordCrash(String tag, Throwable throwable) {
+        Context context = appContext;
+        if (context == null || throwable == null) {
+            return;
+        }
+        long ts = System.currentTimeMillis();
+        String stack = safeText(Log.getStackTraceString(throwable), MAX_STACK_CHARS);
+        String hint = safeText(throwable.getClass().getSimpleName() + ": " + throwable.getMessage(), 300);
+        SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+        sp.edit()
+                .putString(KEY_LAST_CRASH_HINT, hint)
+                .putLong(KEY_LAST_CRASH_AT, ts)
+                .putString(KEY_LAST_CRASH_STACK, stack)
+                .apply();
+        log("ERROR", tag, hint, throwable, null, true);
+    }
+
+    public static JSONObject getFeedbackBundle() {
+        Context context = appContext;
+        JSONObject res = new JSONObject();
+        if (context == null) {
+            res.put("clientLog", "");
+            res.put("clientCrashHint", "");
+            res.put("clientCrashAt", 0L);
+            return res;
+        }
+        SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+        res.put("clientLog", getRecentLogText(MAX_EXPORT_CHARS));
+        res.put("clientCrashHint", sp.getString(KEY_LAST_CRASH_HINT, ""));
+        res.put("clientCrashAt", sp.getLong(KEY_LAST_CRASH_AT, 0L));
+        return res;
+    }
+
+    public static JSONObject getCrashInfo() {
+        Context context = appContext;
+        JSONObject res = new JSONObject();
+        if (context == null) {
+            res.put("hint", "");
+            res.put("crashAt", 0L);
+            res.put("stack", "");
+            return res;
+        }
+        SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+        res.put("hint", sp.getString(KEY_LAST_CRASH_HINT, ""));
+        res.put("crashAt", sp.getLong(KEY_LAST_CRASH_AT, 0L));
+        res.put("stack", sp.getString(KEY_LAST_CRASH_STACK, ""));
+        return res;
+    }
+
+    public static void clearCrashInfo() {
+        Context context = appContext;
+        if (context == null) {
+            return;
+        }
+        context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+                .edit()
+                .remove(KEY_LAST_CRASH_HINT)
+                .remove(KEY_LAST_CRASH_AT)
+                .remove(KEY_LAST_CRASH_STACK)
+                .apply();
+    }
+
+    public static String getRecentLogText(int maxChars) {
+        Context context = appContext;
+        if (context == null) {
+            return "";
+        }
+        File dir = getLogDir(context);
+        File[] files = dir.listFiles((file) -> file.isFile() && file.getName().startsWith("native-log-"));
+        if (files == null || files.length == 0) {
+            return "";
+        }
+        Arrays.sort(files, Comparator.comparing(File::getName));
+        StringBuilder sb = new StringBuilder();
+        for (int i = files.length - 1; i >= 0; i--) {
+            try {
+                String text = readFileText(files[i]);
+                if (!TextUtils.isEmpty(text)) {
+                    if (sb.length() > 0) {
+                        sb.insert(0, '\n');
+                    }
+                    sb.insert(0, text.trim());
+                }
+                if (sb.length() >= maxChars) {
+                    break;
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "read log file failed: " + files[i].getAbsolutePath(), e);
+            }
+        }
+        if (sb.length() > maxChars) {
+            return sb.substring(sb.length() - maxChars);
+        }
+        return sb.toString();
+    }
+
+    private static void appendLine(Context context, JSONObject line) {
+        FileOutputStream fos = null;
+        try {
+            File file = new File(getLogDir(context), buildTodayFileName());
+            fos = new FileOutputStream(file, true);
+            fos.write((line.toJSONString() + "\n").getBytes(StandardCharsets.UTF_8));
+        } catch (Exception e) {
+            Log.e(TAG, "appendLine failed", e);
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException ignore) {
+                }
+            }
+        }
+    }
+
+    private static String readFileText(File file) {
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(file);
+            byte[] buf = new byte[(int) file.length()];
+            int len = fis.read(buf);
+            if (len <= 0) {
+                return "";
+            }
+            return new String(buf, 0, len, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            Log.e(TAG, "readFileText failed: " + file.getAbsolutePath(), e);
+            return "";
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException ignore) {
+                }
+            }
+        }
+    }
+
+    private static File getLogDir(Context context) {
+        File dir = new File(context.getFilesDir(), "native-client-logs");
+        if (!dir.exists() && !dir.mkdirs()) {
+            Log.w(TAG, "mkdir failed: " + dir.getAbsolutePath());
+        }
+        return dir;
+    }
+
+    private static String buildTodayFileName() {
+        return "native-log-" + new SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(new Date()) + ".jsonl";
+    }
+
+    private static void cleanupExpiredFiles() {
+        Context context = appContext;
+        if (context == null) {
+            return;
+        }
+        File[] files = getLogDir(context).listFiles((file) -> file.isFile() && file.getName().startsWith("native-log-"));
+        if (files == null) {
+            return;
+        }
+        long cutoff = System.currentTimeMillis() - RETENTION_MS;
+        for (File file : files) {
+            if (file.lastModified() < cutoff && !file.delete()) {
+                Log.w(TAG, "delete expired log failed: " + file.getAbsolutePath());
+            }
+        }
+    }
+
+    private static String safeLevel(String level) {
+        if ("ERROR".equalsIgnoreCase(level) || "WARN".equalsIgnoreCase(level) || "INFO".equalsIgnoreCase(level)) {
+            return level.toUpperCase(Locale.ROOT);
+        }
+        return "INFO";
+    }
+
+    private static String safeTag(String tag) {
+        return safeText(TextUtils.isEmpty(tag) ? "NATIVE" : tag, 80);
+    }
+
+    private static String safeText(String text, int maxLen) {
+        if (text == null) {
+            return "";
+        }
+        String normalized = text.replace('\r', ' ').replace('\n', ' ').trim();
+        if (normalized.length() <= maxLen) {
+            return normalized;
+        }
+        return normalized.substring(0, maxLen);
+    }
+}

+ 14 - 10
uniplugin_module/src/main/java/io/dcloud/uniplugin/db800/HeartRateSdkManager.java

@@ -44,6 +44,8 @@ import no.nordicsemi.android.support.v18.scanner.ScanSettings;
 import pub.devrel.easypermissions.AfterPermissionGranted;
 import pub.devrel.easypermissions.AfterPermissionGranted;
 import pub.devrel.easypermissions.EasyPermissions;
 import pub.devrel.easypermissions.EasyPermissions;
 
 
+import io.dcloud.uniplugin.AppLog;
+
 public class HeartRateSdkManager {
 public class HeartRateSdkManager {
 
 
    private static final String TAG = "HeartRateSdkManager";
    private static final String TAG = "HeartRateSdkManager";
@@ -95,7 +97,7 @@ public class HeartRateSdkManager {
       mContext = _context;
       mContext = _context;
       handler = new Handler(Looper.getMainLooper());
       handler = new Handler(Looper.getMainLooper());
       checkAndroidVersion();
       checkAndroidVersion();
-      Log.i(TAG, "Init HeartRateSdkManager.");
+      AppLog.i("NATIVE_BLE", "Init HeartRateSdkManager");
    }
    }
 
 
    // 设置回调
    // 设置回调
@@ -106,7 +108,7 @@ public class HeartRateSdkManager {
    // 扫描蓝牙设备并且进行连接操作
    // 扫描蓝牙设备并且进行连接操作
    public void startBluetoothOperations() {
    public void startBluetoothOperations() {
       if(isScanning || isConnected) {
       if(isScanning || isConnected) {
-         Log.d(TAG, "Bluetooth isScanning or isConnected.");
+         AppLog.w("NATIVE_BLE", "startBluetoothOperations ignored: isScanning or isConnected");
          return;
          return;
       };
       };
       // 创建扫描设置
       // 创建扫描设置
@@ -117,6 +119,7 @@ public class HeartRateSdkManager {
       BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
       BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
       scanner.startScan(null, settings, scanCallback);
       scanner.startScan(null, settings, scanCallback);
       isScanning = true; // 开始扫描,设置标志位
       isScanning = true; // 开始扫描,设置标志位
+      AppLog.i("NATIVE_BLE", "scan started");
    }
    }
    // 统一管理扫描和连接状态的操作
    // 统一管理扫描和连接状态的操作
    public void stopBluetoothOperations() {
    public void stopBluetoothOperations() {
@@ -133,7 +136,7 @@ public class HeartRateSdkManager {
          BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
          BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
          scanner.stopScan(scanCallback);
          scanner.stopScan(scanCallback);
          isScanning = false; // 停止扫描,重置标志位
          isScanning = false; // 停止扫描,重置标志位
-         Log.i(TAG, "Stopped scanning.");
+         AppLog.i("NATIVE_BLE", "scan stopped");
       }
       }
    }
    }
    // 断开蓝牙连接
    // 断开蓝牙连接
@@ -147,9 +150,9 @@ public class HeartRateSdkManager {
          }
          }
          bluetoothGatt.disconnect();
          bluetoothGatt.disconnect();
          isConnected = false;
          isConnected = false;
-         Log.i(TAG, "Disconnect initiated from device.");
+         AppLog.i("NATIVE_BLE", "disconnect initiated");
       } else {
       } else {
-         Log.w(TAG, "BluetoothGatt is null or already disconnected.");
+         AppLog.w("NATIVE_BLE", "disconnect ignored: gatt null or already disconnected");
       }
       }
    }
    }
 
 
@@ -172,17 +175,18 @@ public class HeartRateSdkManager {
          if (deviceName != null && isNameInFilter(deviceName)) {
          if (deviceName != null && isNameInFilter(deviceName)) {
             // Android 8.0+:仅对可连接设备发起连接,不可连接的(如已被其他设备连接)跳过
             // Android 8.0+:仅对可连接设备发起连接,不可连接的(如已被其他设备连接)跳过
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !result.isConnectable()) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !result.isConnectable()) {
-               Log.d(TAG, "Skip device (not connectable): " + deviceName + " [" + device.getAddress() + "]");
+               AppLog.w("NATIVE_BLE", "Skip device (not connectable): " + deviceName);
                return;
                return;
             }
             }
             stopScan();
             stopScan();
             scannerPacket = new ScannerPacket(result);
             scannerPacket = new ScannerPacket(result);
+            AppLog.i("NATIVE_BLE", "connectToDevice: " + deviceName);
             connectToDevice(device); // 连接蓝牙设备
             connectToDevice(device); // 连接蓝牙设备
          }
          }
       }
       }
       @Override
       @Override
       public void onScanFailed(int errorCode) {
       public void onScanFailed(int errorCode) {
-         Log.e(TAG, "Scan failed with error: " + errorCode);
+         AppLog.e("NATIVE_BLE", "Scan failed with error: " + errorCode);
          showMsg("扫描错误:"+errorCode);
          showMsg("扫描错误:"+errorCode);
       }
       }
    };
    };
@@ -214,18 +218,18 @@ public class HeartRateSdkManager {
 
 
       // requestPermissions 必须在 Activity + 主线程上执行,且避免多处调用导致连发
       // requestPermissions 必须在 Activity + 主线程上执行,且避免多处调用导致连发
       if (!(mContext instanceof Activity)) {
       if (!(mContext instanceof Activity)) {
-         Log.w(TAG, "requestBluetoothPermissions skipped: mContext is not Activity");
+         AppLog.w("NATIVE_PERMISSION", "requestBluetoothPermissions skipped: mContext is not Activity");
          return;
          return;
       }
       }
       final Activity activity = (Activity) mContext;
       final Activity activity = (Activity) mContext;
       if (activity.isFinishing() || activity.isDestroyed()) {
       if (activity.isFinishing() || activity.isDestroyed()) {
-         Log.w(TAG, "requestBluetoothPermissions skipped: activity finishing/destroyed");
+         AppLog.w("NATIVE_PERMISSION", "requestBluetoothPermissions skipped: activity finishing/destroyed");
          return;
          return;
       }
       }
 
 
       long now = android.os.SystemClock.elapsedRealtime();
       long now = android.os.SystemClock.elapsedRealtime();
       if (isRequestingBluetoothPermissions || (now - lastPermissionRequestAtMs) < PERMISSION_DEBOUNCE_MS) {
       if (isRequestingBluetoothPermissions || (now - lastPermissionRequestAtMs) < PERMISSION_DEBOUNCE_MS) {
-         Log.d(TAG, "requestBluetoothPermissions debounced");
+         AppLog.w("NATIVE_PERMISSION", "requestBluetoothPermissions debounced");
          return;
          return;
       }
       }
       isRequestingBluetoothPermissions = true;
       isRequestingBluetoothPermissions = true;

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff