#include <dirent.h>
#include "custom_utils.h"
#include <sys/time.h>
+#include "mbedtls/md5.h"
+#ifdef SUPPORT_CAT1_SERVICE
+#include "cat1_pub_api.h"
+#endif
/* 宏定义 */
#define MAX_PATH_LEN 512 // 最大路径长度
char *emotion; // 心情类型(动态分配)
char *emojiUrl; // 表情文件URL(动态分配)
long long size; // 表情文件大小(字节,支持断点续传判断;0 表示未知)
+ char *md5; // MD5校验值(动态分配,可为NULL)
} EmotionEmoji;
/* 表情包配置结构体 */
/* 前置声明,便于在本节工具函数中调用 */
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);
+/* MD5计算和校验函数 */
+static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33]);
+static int ln_emoji_verify_md5(const char *file_path, const char *expected_md5);
/* 毫秒时间戳 */
static inline unsigned long long ln_now_ms(void)
{
free(g_emoji_package.emojis[i].emojiUrl);
g_emoji_package.emojis[i].emojiUrl = NULL;
}
+ if (g_emoji_package.emojis[i].md5)
+ {
+ free(g_emoji_package.emojis[i].md5);
+ g_emoji_package.emojis[i].md5 = NULL;
+ }
}
free(g_emoji_package.emojis);
g_emoji_package.emojis = NULL;
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 生成表情缓存路径成功,path=%s", path);
}
+/* 计算文件的MD5值(小写十六进制字符串) */
+static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33])
+{
+ if (!file_path || !out_hex) return -1;
+ FILE *fp = fopen(file_path, "rb");
+ if (!fp) return -1;
+ mbedtls_md5_context ctx;
+ mbedtls_md5_init(&ctx);
+ if (mbedtls_md5_starts(&ctx) != 0) { mbedtls_md5_free(&ctx); fclose(fp); return -1; }
+ unsigned char buf[1024];
+ size_t n;
+ while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
+ if (mbedtls_md5_update(&ctx, buf, n) != 0) { mbedtls_md5_free(&ctx); fclose(fp); return -1; }
+ }
+ fclose(fp);
+ unsigned char md5_ctx_buf[16];
+ if (mbedtls_md5_finish(&ctx, md5_ctx_buf) != 0) { mbedtls_md5_free(&ctx); return -1; }
+ mbedtls_md5_free(&ctx);
+ static const char hex[] = "0123456789abcdef";
+ for (int i = 0; i < 16; i++) {
+ out_hex[i * 2] = hex[(md5_ctx_buf[i] >> 4) & 0x0F];
+ out_hex[i * 2 + 1] = hex[md5_ctx_buf[i] & 0x0F];
+ }
+ out_hex[32] = '\0';
+ return 0;
+}
+
+/* 校验文件的MD5值 */
+static int ln_emoji_verify_md5(const char *file_path, const char *expected_md5)
+{
+ if (file_path == NULL || expected_md5 == NULL || expected_md5[0] == '\0') {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify: invalid args");
+ return -1;
+ }
+ char calc[33] = {0};
+ if (ln_calc_file_md5_lower_hex(file_path, calc) != 0) {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify: calc failed for %s", file_path);
+ return -1;
+ }
+ char expect_lower[33] = {0};
+ size_t elen = strlen(expected_md5);
+ size_t copy_len = elen > 32 ? 32 : elen;
+ for (size_t i = 0; i < copy_len; i++) {
+ char ch = expected_md5[i];
+ if (ch >= 'A' && ch <= 'Z') ch = (char)(ch - 'A' + 'a');
+ expect_lower[i] = ch;
+ }
+ expect_lower[32] = '\0';
+ int ok = (strcmp(calc, expect_lower) == 0) ? 1 : 0;
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify result=%s, calc=%s, expect=%s",
+ ok ? "OK" : "FAIL", calc, expect_lower);
+ return ok;
+}
+
/* 工具函数:检查表情是否已缓存 */
static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji)
{
char path[MAX_PATH_LEN] = {0};
ln_gen_emoji_cache_path(pkg, emoji, path, sizeof(path));
struct stat st;
- return (stat(path, &st) == 0 && S_ISREG(st.st_mode)) ? 1 : 0;
+
+ /* 检查文件是否存在,同时获取文件大小(stat是元数据操作,很快) */
+ if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
+ return 0;
+ }
+
+ /* 优先检查文件大小(使用已获取的st.st_size,避免额外IO)
+ * 如果大小不匹配,直接返回,避免读取整个文件计算MD5 */
+ if (emoji->size > 0) {
+ unsigned long long file_size = (unsigned long long)st.st_size;
+ if (file_size != (unsigned long long)emoji->size) {
+ /* 文件大小不匹配,可能不完整,删除文件并返回0以触发重新下载 */
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 文件大小不匹配(期望:%lld, 实际:%llu),删除不完整文件: %s",
+ emoji->emotion ? emoji->emotion : "unknown", emoji->size, file_size, path);
+ unlink(path);
+ return 0;
+ }
+ }
+
+ /* 文件大小匹配后,再进行MD5校验(需要读取整个文件) */
+ if (emoji->md5 != NULL && emoji->md5[0] != '\0') {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 检查已缓存文件 %s 的MD5,期望MD5=%s",
+ emoji->emotion ? emoji->emotion : "unknown", emoji->md5);
+ int md5_ok = ln_emoji_verify_md5(path, emoji->md5);
+ if (md5_ok != 1) {
+ /* MD5校验失败,文件可能不完整,删除文件并返回0以触发重新下载 */
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败,删除不完整文件: %s",
+ emoji->emotion ? emoji->emotion : "unknown", path);
+ unlink(path);
+ return 0;
+ } else {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 已缓存文件MD5校验通过",
+ emoji->emotion ? emoji->emotion : "unknown");
+ }
+ }
+
+ return 1;
}
/* 批量下载表情并上报结果 */
}
}
downloaded_count = cached_count;
- WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d", downloaded_count);
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d/%d", downloaded_count, pkg->emojiCount);
if (pkg->emojiCount > 0)
{
int init_progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100);
+ // 确保进度在0-100范围内
+ if (init_progress < 0) init_progress = 0;
+ if (init_progress > 100) init_progress = 100;
if (init_progress > 0)
{
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始进度=%d%%", init_progress);
push_download_progress_notification(init_progress);
}
}
break;
}
- if (uapi_ln_download_file_resume_with_retry(pkg->emojis[i].emojiUrl, save_path, 10) != ERRCODE_SUCC)
+ int download_success = 0;
+ int max_retry = 2; // 最大重试次数(包括MD5校验失败后的重试),减少重试次数以加快失败处理
+ int retry_count = 0;
+
+ while (retry_count < max_retry)
{
- // 检查是否因为取消而失败
- if (g_emoji_download_cancelled)
+ // 如果是重试,先删除旧文件
+ if (retry_count > 0)
{
- WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载被取消");
- all_success = 0;
- break;
+ unlink(save_path);
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 第%d次重试下载", pkg->emojis[i].emotion, retry_count);
}
+
+ // 减少内部重试次数,从10次减少到3次,避免单个文件耗时过长
+ if (uapi_ln_download_file_resume_with_retry(pkg->emojis[i].emojiUrl, save_path, 3) != ERRCODE_SUCC)
+ {
+ // 检查是否因为取消而失败
+ if (g_emoji_download_cancelled)
+ {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载被取消");
+ all_success = 0;
+ break;
+ }
- all_success = 0;
- failed_count++;
- WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载失败", pkg->emojis[i].emotion);
- // 上报下载失败状态
- char fail_msg[128];
- snprintf(fail_msg, sizeof(fail_msg), "表情 %s 下载失败", pkg->emojis[i].emotion);
- ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg);
- /* 单个文件三次重试仍失败,终止后续下载并尽快上报失败 */
+ retry_count++;
+ if (retry_count >= max_retry)
+ {
+ // 单个文件重试次数已用完,立即跳过该文件,继续下载下一个文件
+ all_success = 0;
+ failed_count++;
+ // 删除可能存在的部分下载文件,避免留下不完整的文件
+ unlink(save_path);
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载失败(已重试%d次),删除不完整文件,跳过该文件,继续下载下一个", pkg->emojis[i].emotion, retry_count);
+ // 上报下载失败状态
+ char fail_msg[128];
+ snprintf(fail_msg, sizeof(fail_msg), "表情 %s 下载失败", pkg->emojis[i].emotion);
+ ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg);
+ // 跳过该文件,继续下载下一个文件(不终止整个下载任务)
+ break; // 退出while循环,继续for循环的下一个文件
+ }
+ // 重试前等待一段时间,避免频繁重试
+ sleep(2);
+ continue; // 继续重试
+ }
+
+ // 下载成功,进行MD5校验(如果有MD5值)
+ if (pkg->emojis[i].md5 && pkg->emojis[i].md5[0] != '\0')
+ {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成,开始MD5校验,期望MD5=%s",
+ pkg->emojis[i].emotion, pkg->emojis[i].md5);
+
+ if (ln_emoji_verify_md5(save_path, pkg->emojis[i].md5) != 1)
+ {
+ // MD5校验失败,删除文件并重试
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败,删除文件并重试", pkg->emojis[i].emotion);
+ unlink(save_path);
+ retry_count++;
+ if (retry_count >= max_retry)
+ {
+ // MD5校验失败且重试次数已用完,立即跳过该文件,继续下载下一个文件
+ all_success = 0;
+ failed_count++;
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败(已重试%d次),跳过该文件,继续下载下一个", pkg->emojis[i].emotion, retry_count);
+ char fail_msg[128];
+ snprintf(fail_msg, sizeof(fail_msg), "表情 %s MD5校验失败", pkg->emojis[i].emotion);
+ ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg);
+ break; // 退出while循环,继续for循环的下一个文件
+ }
+ // 重试前等待一段时间,避免频繁重试
+ sleep(2);
+ continue; // 继续重试
+ }
+ else
+ {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验通过", pkg->emojis[i].emotion);
+ }
+ }
+ else
+ {
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成(无MD5校验值,跳过校验)", pkg->emojis[i].emotion);
+ }
+
+ // 下载和校验都成功
+ download_success = 1;
break;
}
- else
+
+ // 处理下载结果(成功或失败)
+ if (download_success)
{
downloaded_count++;
/* 表情进度上报,按下载成功表情数量计算 */
int progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100);
+ // 确保进度在0-100范围内
+ if (progress < 0) progress = 0;
+ if (progress > 100) progress = 100;
+
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载并校验完成, downloaded_count=%d/%d, progress=%d%%",
+ pkg->emojis[i].emotion, downloaded_count, pkg->emojiCount, progress);
+
+ // 更新UI进度
push_download_progress_notification(progress);
- WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成", pkg->emojis[i].emotion);
// 每次进度更新都同步发送协议给服务器更新进度
char progress_msg[64];
// 反初始化HTTP文件下载服务
http_file_download_deinit();
}
-
+/**
+ * 带md5sum的参考格式
+ * 解析时要注意带md5和不带md5的要兼容。
+ * {"deviceId":"867998086628226","timestamp":1767672001,"messageId":"18692b900b384a40","command":"DownloadEmoji","data":{"characterId":"de50ca21420f","eyesColor":"brown","eyesShape":"calm-eyes","emotionEmojis":[{"emotion":"happy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Smile.mp4","size":715594,"md5":"262d7d33f273c0aafa137e2d72d2eaf9"},{"emotion":"laughing","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Laugh.mp4","size":773624,"md5":"7d74df7f75661452ce3d1e51e2bd3d58"},{"emotion":"angry","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Angry.mp4","size":754492,"md5":"b4e6f8ac6c0d20c1e71336ca10d82ffa"},{"emotion":"wronged","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Wronged.mp4","size":1169130,"md5":"62f651821a12b224bd0b9e4d61ad16d8"},{"emotion":"heartbeat","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Heartbeat.mp4","size":940392,"md5":"2e4df95c116e82f4ab31ed21b6d7fc49"},{"emotion":"awkward","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Embarrassed.mp4","size":793970,"md5":"e168cc2e789732c61653c91cb72a3a27"},{"emotion":"confused","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Doubt.mp4","size":812766,"md5":"70c2c027e261394c78dbaaff5284829c"},{"emotion":"shocked","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Shocked.mp4","size":843018,"md5":"decc6a03f7d1356a6f3f82f57636bf0a"},{"emotion":"crying","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Tears.mp4","size":898970,"md5":"369e17f42842db9c0b1a330800416760"},{"emotion":"blowing-kiss","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Kiss.mp4","size":539583,"md5":"d1dc573d076a59ffae8fbdbb9eceb432"},{"emotion":"sleepy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Sleepy.mp4","size":661627,"md5":"2483cc4a1aa57b9b15b8ab3d17bf453b"},{"emotion":"mischievous","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Naughty.mp4","size":636318,"md5":"efe8d0d6d10bebf32cfe10689cba8702"},{"emotion":"speechless","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Speechless.mp4","size":875623,"md5":"a58d5ba63375732a8259f294dfe35a92"},{"emotion":"dizzy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Dizzy.mp4","size":1133467,"md5":"7f6988e7509655d27ac4a0e53859930b"},{"emotion":"thinking","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Think.mp4","size":960243,"md5":"b09dc26e2a9b1ece3609bb5c019c5619"},{"emotion":"working","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/WorkAndStudy.mp4","size":1029099,"md5":"bc483c6728b3767cc1bd5dff7a1a2411"},{"emotion":"exercising","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Exercise.mp4","size":1056919,"md5":"65382bf4fc5a357dcc963d4dc4495ec8"},{"emotion":"playing","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Play.mp4","size":1077517,"md5":"d1a379980e2c144b02c915b41711e62f"},{"emotion":"daydreaming","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Daze.mp4","size":1034574,"md5":"f71e5d96ba3afff009ff70ef0f39ebe8"}],"size":16706926}}
+ */
/* 解析下载表情命令JSON */
void ln_parse_download_emoji(const char *json_buf, int json_len)
{
cJSON *emotion = cJSON_GetObjectItem(item, "emotion");
cJSON *emojiUrl = cJSON_GetObjectItem(item, "emojiUrl");
cJSON *size_node = cJSON_GetObjectItem(item, "size");
+ cJSON *md5_node = cJSON_GetObjectItem(item, "md5");
g_emoji_package.emojis[i].emotion = ln_safe_strdup((emotion && cJSON_IsString(emotion)) ? emotion->valuestring : "");
g_emoji_package.emojis[i].emojiUrl = ln_safe_strdup((emojiUrl && cJSON_IsString(emojiUrl)) ? emojiUrl->valuestring : "");
+
/* 解析单个表情大小(字节) */
long long emoji_size = 0;
if (size_node)
}
}
g_emoji_package.emojis[i].size = (emoji_size > 0) ? emoji_size : 0;
- WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, " emoji %d: emotion=%s, url=%s, size=%lld",
+
+ /* 解析MD5校验值 */
+ if (md5_node && cJSON_IsString(md5_node) && md5_node->valuestring)
+ {
+ g_emoji_package.emojis[i].md5 = ln_safe_strdup(md5_node->valuestring);
+ }
+ else
+ {
+ g_emoji_package.emojis[i].md5 = NULL;
+ }
+
+ /* 打印解析到的size和md5信息 */
+ WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, " emoji %d: emotion=%s, url=%s, size=%lld, md5=%s",
i + 1,
g_emoji_package.emojis[i].emotion,
g_emoji_package.emojis[i].emojiUrl,
- g_emoji_package.emojis[i].size);
+ g_emoji_package.emojis[i].size,
+ g_emoji_package.emojis[i].md5 ? g_emoji_package.emojis[i].md5 : "NULL");
}
}
}