表情下载添加预估时间功能
authorluozheng <1732036298@qq.com>
Wed, 29 Oct 2025 08:46:35 +0000 (16:46 +0800)
committerluozheng <1732036298@qq.com>
Wed, 29 Oct 2025 08:46:35 +0000 (16:46 +0800)
code/sdk/application/wearable/ln/nativeapp/nativeui/include/uiservice/ui_service.h
code/sdk/application/wearable/ln/nativeapp/nativeui/ota/include/OtaDownloadView.h
code/sdk/application/wearable/ln/nativeapp/nativeui/ota/include/OtaModel.h
code/sdk/application/wearable/ln/nativeapp/nativeui/ota/src/OtaDownloadView.cpp
code/sdk/application/wearable/ln/nativeapp/nativeui/ota/src/OtaModel.cpp
code/sdk/application/wearable/ln/nativeapp/nativeui/uiservice/src/ui_service.cpp
code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c
code/sdk/zhihui/leiniao/ln_api_cpp/ln_api_interface.cpp
code/sdk/zhihui/leiniao/ln_api_cpp/ln_api_interface.h

index 8966aef..4c3fb31 100644 (file)
@@ -65,6 +65,7 @@ typedef enum {
     HAND_DOWNLOAD_SUCCESS_NOTIFY, // 下载成功通知
     HAND_DOWNLOAD_FAIL_NOTIFY, // 下载失败通知
     HAND_DOWNLOAD_PROGRESS_NOTIFY, // 下载进度通知
+    HAND_DOWNLOAD_ETA_NOTIFY, // 下载ETA(剩余时间)通知,单位秒,-1表示未知
     HAND_AUDIO_PLAYBACK_START_NOTIFY, // 音频播放开始通知
     HAND_AUDIO_PLAYBACK_END_NOTIFY, // 音频播放结束通知
     HAND_AUDIO_RECORDING_END_NOTIFY, // 录音结束通知
index cd1b4e0..233a489 100644 (file)
@@ -33,6 +33,9 @@ namespace OHOS
         
         // 更新下载状态文本
         void UpdateDownloadStatus(const char* status);
+
+        // 更新下载ETA
+        void UpdateDownloadEta(const char* eta);
         
         // 设置下载类型
         void SetDownloadType(int type);
index fe2e0c7..d555ffe 100644 (file)
@@ -70,6 +70,7 @@ namespace OHOS
 
         void HandleDownloadSuccess(int type);
         void HandleDownloadProgress(int progress);  
+        void HandleDownloadEta(int seconds);
         void HandleDownloadFail(int type);
     private:
         OtaModel();
index 2ab1584..1608b81 100644 (file)
@@ -90,7 +90,7 @@ namespace OHOS
             RemindLabel->SetFont(DEFAULT_VECTOR_FONT_FILENAME, 18);
             RemindLabel->SetStyle(STYLE_TEXT_COLOR, 0xffffffff);
         
-            RemindLabel->SetText("预计需要18分钟");
+            // RemindLabel->SetText("预计需要18分钟");
             RemindLabel->SetVisible(true);
             group->Add(RemindLabel);
         }
@@ -144,6 +144,14 @@ namespace OHOS
         }
     }
 
+    void OtaDownloadView::UpdateDownloadEta(const char* eta)
+    {
+        if (RemindLabel && eta) {
+            RemindLabel->SetText(eta);
+            RemindLabel->Invalidate();
+            WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, "OtaDownloadView::UpdateDownloadEta: %s", eta);
+        }
+    }
     void OtaDownloadView::SetDownloadType(int type)
     {
         downloadType = type;
index 0fff649..8a62942 100644 (file)
@@ -427,6 +427,26 @@ namespace OHOS
         }
     }
 
+    // 下载ETA(单位秒,-1表示未知):用于在下载界面展示预估剩余时间
+    void OtaModel::HandleDownloadEta(int seconds)
+    {
+        if(OtaDownloadView::GetInstance())
+        {
+            char buf[64] = {0};
+            if (seconds >= 0)
+            {
+                //只显示分钟,不显示秒
+                int mm = seconds / 60;
+                (void)snprintf(buf, sizeof(buf) - 1, "预计需要%d分钟", mm);
+            }
+            else
+            {
+                (void)snprintf(buf, sizeof(buf) - 1, "预计需要 —");
+            }
+            OtaDownloadView::GetInstance()->UpdateDownloadEta(buf);
+        }
+    }
+
     //下载失败通知
     void OtaModel::HandleDownloadFail(int type)
     {
index 4ceb644..201e412 100644 (file)
@@ -101,6 +101,7 @@ static void HandDownloadFileNotify(Request *request);
 static void HandDownloadSuccessNotify(Request *request);
 static void HandDownloadFailNotify(Request *request);
 static void HandDownloadProgressNotify(Request *request);
+static void HandDownloadEtaNotify(Request *request);
 static void HandAudioPlaybackStartNotify(Request *request);
 static void HandAudioPlaybackEndNotify(Request *request);
 static void HandAudioRecordingEndNotify(Request *request);
@@ -123,6 +124,7 @@ MsgHandlerEntry msgHandlers[] = {
     {HAND_DOWNLOAD_SUCCESS_NOTIFY,HandDownloadSuccessNotify},
     {HAND_DOWNLOAD_FAIL_NOTIFY,HandDownloadFailNotify},
     {HAND_DOWNLOAD_PROGRESS_NOTIFY,HandDownloadProgressNotify},
+    {HAND_DOWNLOAD_ETA_NOTIFY,HandDownloadEtaNotify},
     {HAND_AUDIO_PLAYBACK_START_NOTIFY,HandAudioPlaybackStartNotify},
     {HAND_AUDIO_PLAYBACK_END_NOTIFY,HandAudioPlaybackEndNotify},
     {HAND_AUDIO_RECORDING_END_NOTIFY,HandAudioRecordingEndNotify},
@@ -636,6 +638,16 @@ void HandDownloadProgressNotify(Request *request)
         std::bind(&OHOS::OtaModel::HandleDownloadProgress,OHOS::OtaModel::GetInstance(), progress));
 }
 
+// 下载ETA通知(单位秒,-1表示未知)
+void HandDownloadEtaNotify(Request *request)
+{
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_UI_SERVICE, "HandDownloadEtaNotify msg_id = %d", request->msgId);
+    int seconds = *(int *)request->data;
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_UI_SERVICE, "HandDownloadEtaNotify seconds = %d", seconds);
+    GraphicService::GetInstance()->PostGraphicEvent(
+        std::bind(&OHOS::OtaModel::HandleDownloadEta,OHOS::OtaModel::GetInstance(), seconds));
+}
+
 // 下载失败通知
 void HandDownloadFailNotify(Request *request)
 {
index 08972b4..b2f4146 100644 (file)
@@ -9,6 +9,7 @@
 #include <errno.h>
 #include <dirent.h>
 #include "custom_utils.h"
+#include <sys/time.h>
 
 /* 宏定义 */
 #define MAX_PATH_LEN 512 // 最大路径长度
@@ -34,6 +35,62 @@ typedef struct
 
 /* 全局变量 */
 EmojiPackage g_emoji_package = {0};
+/* ========== ETA 粗略版辅助函数 ========== */
+/* 前置声明,便于在本节工具函数中调用 */
+static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji);
+static void ln_gen_emoji_cache_path(const EmojiPackage *pkg, const EmotionEmoji *emoji, char *path, size_t path_len);
+/* 毫秒时间戳 */
+static inline unsigned long long ln_now_ms(void)
+{
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    return (unsigned long long)tv.tv_sec * 1000ULL + (unsigned long long)tv.tv_usec / 1000ULL;
+}
+
+/* 文件大小(不存在返回0)*/
+static unsigned long long ln_get_file_size(const char *path)
+{
+    if (!path)
+        return 0;
+    struct stat st;
+    if (stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0)
+        return (unsigned long long)st.st_size;
+    return 0;
+}
+
+/* 计算表情包总字节:优先用服务端 totalSize,否则汇总每个emoji的 size */
+static unsigned long long ln_calc_total_bytes_from_pkg(const EmojiPackage *pkg)
+{
+    if (!pkg)
+        return 0;
+    if (pkg->totalSize > 0)
+        return (unsigned long long)pkg->totalSize;
+    unsigned long long sum = 0;
+    for (int i = 0; i < pkg->emojiCount; ++i)
+    {
+        if (pkg->emojis[i].size > 0)
+            sum += (unsigned long long)pkg->emojis[i].size;
+    }
+    return sum;
+}
+
+/* 计算已缓存完整文件的总字节 */
+static unsigned long long ln_calc_cached_full_bytes(const EmojiPackage *pkg)
+{
+    if (!pkg)
+        return 0;
+    unsigned long long sum = 0;
+    for (int i = 0; i < pkg->emojiCount; ++i)
+    {
+        if (ln_emoji_is_cached(pkg, &pkg->emojis[i]))
+        {
+            char path[MAX_PATH_LEN] = {0};
+            ln_gen_emoji_cache_path(pkg, &pkg->emojis[i], path, sizeof(path));
+            sum += ln_get_file_size(path);
+        }
+    }
+    return sum;
+}
 
 /* 安全字符串复制函数 */
 static inline void ln_safe_strcpy(char *dst, size_t dst_size, const char *src)
@@ -379,6 +436,16 @@ static void ln_emoji_download(void *arg)
     int downloaded_count = 0; /* 包含已缓存与成功下载的 */
     int failed_count = 0;
 
+    /* 粗略版 ETA 统计(本次会话)*/
+    unsigned long long batch_start_ms = ln_now_ms();
+    unsigned long long total_bytes_all = ln_calc_total_bytes_from_pkg(pkg);
+    unsigned long long cached_full_bytes = 0;
+    unsigned long long session_downloaded_bytes = 0; /* 仅统计本次会话新下载的字节 */
+    /* 速率平滑(EMA)*/
+    double ema_bps = 0.0;
+    int ema_ready = 0;
+    const double ETA_WARMUP_SECONDS = 5.0; /* 预热期内不推ETA或推未知,避免初期抖动 */
+
     /* 若有总大小信息,则进行磁盘空间预检查(考虑已缓存文件大小) */
     if (pkg->totalSize > 0)
     {
@@ -399,6 +466,13 @@ static void ln_emoji_download(void *arg)
         if (ln_emoji_is_cached(pkg, &pkg->emojis[ci]))
         {
             cached_count++;
+            /* 统计完整缓存字节,用于 ETA 剩余估算 */
+            if (total_bytes_all > 0)
+            {
+                char path_bytes[MAX_PATH_LEN] = {0};
+                ln_gen_emoji_cache_path(pkg, &pkg->emojis[ci], path_bytes, sizeof(path_bytes));
+                cached_full_bytes += ln_get_file_size(path_bytes);
+            }
         }
     }
     downloaded_count = cached_count;
@@ -425,6 +499,8 @@ static void ln_emoji_download(void *arg)
         // 下载并缓存(支持断点续传:当目标文件存在且小于应有大小时,从断点续传)
         char save_path[MAX_PATH_LEN] = {0};
         ln_gen_emoji_cache_path(pkg, &pkg->emojis[i], save_path, sizeof(save_path));
+        unsigned long long pre_size = ln_get_file_size(save_path); /* 记录下载前已有的部分大小(可能为0)*/
+        unsigned long long file_start_ms = ln_now_ms();
         /* 确保保存目录存在 */
         {
             char dir_buf[MAX_PATH_LEN] = {0};
@@ -452,6 +528,83 @@ static void ln_emoji_download(void *arg)
             int progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100);
             push_download_progress_notification(progress);
             WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成", pkg->emojis[i].emotion);
+
+            /* 粗略版 ETA(平滑版):在单个文件完成后估算一次 */
+            if (total_bytes_all > 0)
+            {
+                unsigned long long post_size = ln_get_file_size(save_path);
+                unsigned long long inc = 0;
+                if (post_size > pre_size)
+                    inc = post_size - pre_size;
+                else if ((unsigned long long)pkg->emojis[i].size > 0)
+                    inc = (unsigned long long)pkg->emojis[i].size;
+                else
+                    inc = post_size; /* 兜底 */
+
+                session_downloaded_bytes += inc;
+
+                unsigned long long now_ms = ln_now_ms();
+                double elapsed_s = (now_ms > batch_start_ms) ? ((double)(now_ms - batch_start_ms) / 1000.0) : 0.0;
+                double avg_bps = (elapsed_s > 0.0) ? ((double)session_downloaded_bytes / elapsed_s) : 0.0;
+
+                /* 单文件瞬时速率,用于更新EMA */
+                double file_elapsed_s = (now_ms > file_start_ms) ? ((double)(now_ms - file_start_ms) / 1000.0) : 0.0;
+                if (file_elapsed_s > 0.0 && inc > 0)
+                {
+                    double inst_bps = (double)inc / file_elapsed_s;
+                    if (!ema_ready)
+                    {
+                        ema_bps = inst_bps;
+                        ema_ready = 1;
+                    }
+                    else
+                    {
+                        const double alpha = 0.3; /* 平滑系数:越大越敏感 */
+                        ema_bps = alpha * inst_bps + (1.0 - alpha) * ema_bps;
+                    }
+                }
+
+                /* 选择用于ETA的速率:优先EMA,其次全局平均 */
+                double used_bps = ema_ready ? ema_bps : avg_bps;
+                /* 轻度夹紧,避免异常跳变(以全局平均为中心)*/
+                if (avg_bps > 0.0 && used_bps > 0.0)
+                {
+                    double lower = avg_bps * 0.5;
+                    double upper = avg_bps * 2.0;
+                    if (used_bps < lower) used_bps = lower;
+                    if (used_bps > upper) used_bps = upper;
+                }
+
+                unsigned long long consumed = cached_full_bytes + session_downloaded_bytes;
+                unsigned long long remain_bytes = (total_bytes_all > consumed) ? (total_bytes_all - consumed) : 0ULL;
+                int eta_sec = -1;
+                /* 预热期内(或样本太少)不上报具体ETA,之后用平滑速率估算 */
+                if (elapsed_s >= ETA_WARMUP_SECONDS || downloaded_count > 1)
+                {
+                    if (used_bps > 1.0)
+                        eta_sec = (int)(remain_bytes / used_bps);
+                }
+
+                if (eta_sec >= 0)
+                {
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP,
+                                  "[DOWNLOAD][ETA] progress=%d%%, remain_bytes=%llu, avg=%.1f KB/s, eta=%ds",
+                                  progress,
+                                  (unsigned long long)remain_bytes,
+                                  used_bps / 1024.0,
+                                  eta_sec);
+                    push_download_eta_notification(eta_sec);
+                }
+                else
+                {
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP,
+                                  "[DOWNLOAD][ETA] progress=%d%%, remain_bytes=%llu, avg=%.1f KB/s, eta=未知",
+                                  progress,
+                                  (unsigned long long)remain_bytes,
+                                  (avg_bps > 0.0 ? avg_bps : used_bps) / 1024.0);
+                    push_download_eta_notification(-1);
+                }
+            }
         }
     }
 
index d9a104e..cf8e931 100644 (file)
@@ -254,6 +254,13 @@ void push_download_progress_notification(int progress)
     SendMsgToUIService(HAND_DOWNLOAD_PROGRESS_NOTIFY, 0, &progress, sizeof(progress));
 }
 
+// 推送文件下载ETA(秒)到UI(-1 表示未知)
+void push_download_eta_notification(int seconds)
+{
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_ETA] seconds=%d", seconds);
+    SendMsgToUIService(HAND_DOWNLOAD_ETA_NOTIFY, 0, &seconds, sizeof(seconds));
+}
+
 //通知音频播放开始了
 void push_audio_playback_start_notification(void)
 {
index ea27b27..734d4a9 100644 (file)
@@ -98,6 +98,8 @@ void push_download_success_notification(int type);
 void push_download_fail_notification(int type);
 // 推送文件下载进度通知到UI
 void push_download_progress_notification(int progress);
+// 推送文件下载ETA(秒)到UI(-1 表示未知)
+void push_download_eta_notification(int seconds);
 //通知音频播放结束了
 void push_audio_playback_start_notification(void);
 void push_audio_playback_end_notification(void);