update.c 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. #include <update/update.h>
  2. #include <flip_storage/flip_wifi_storage.h>
  3. #include <storage/storage.h>
  4. static bool update_is_str(const char *src, const char *dst) { return strcmp(src, dst) == 0; }
  5. static bool update_json_to_datetime(DateTime *rtc_time, FuriString *str)
  6. {
  7. if (!rtc_time || !str)
  8. {
  9. FURI_LOG_E(TAG, "rtc_time or str is NULL");
  10. return false;
  11. }
  12. FuriString *hour = get_json_value_furi("hour", str);
  13. if (hour)
  14. {
  15. rtc_time->hour = atoi(furi_string_get_cstr(hour));
  16. furi_string_free(hour);
  17. }
  18. FuriString *minute = get_json_value_furi("minute", str);
  19. if (minute)
  20. {
  21. rtc_time->minute = atoi(furi_string_get_cstr(minute));
  22. furi_string_free(minute);
  23. }
  24. FuriString *second = get_json_value_furi("second", str);
  25. if (second)
  26. {
  27. rtc_time->second = atoi(furi_string_get_cstr(second));
  28. furi_string_free(second);
  29. }
  30. FuriString *day = get_json_value_furi("day", str);
  31. if (day)
  32. {
  33. rtc_time->day = atoi(furi_string_get_cstr(day));
  34. furi_string_free(day);
  35. }
  36. FuriString *month = get_json_value_furi("month", str);
  37. if (month)
  38. {
  39. rtc_time->month = atoi(furi_string_get_cstr(month));
  40. furi_string_free(month);
  41. }
  42. FuriString *year = get_json_value_furi("year", str);
  43. if (year)
  44. {
  45. rtc_time->year = atoi(furi_string_get_cstr(year));
  46. furi_string_free(year);
  47. }
  48. FuriString *weekday = get_json_value_furi("weekday", str);
  49. if (weekday)
  50. {
  51. rtc_time->weekday = atoi(furi_string_get_cstr(weekday));
  52. furi_string_free(weekday);
  53. }
  54. return datetime_validate_datetime(rtc_time);
  55. }
  56. static FuriString *update_datetime_to_json(DateTime *rtc_time)
  57. {
  58. if (!rtc_time)
  59. {
  60. FURI_LOG_E(TAG, "rtc_time is NULL");
  61. return NULL;
  62. }
  63. char json[256];
  64. snprintf(
  65. json,
  66. sizeof(json),
  67. "{\"hour\":%d,\"minute\":%d,\"second\":%d,\"day\":%d,\"month\":%d,\"year\":%d,\"weekday\":%d}",
  68. rtc_time->hour,
  69. rtc_time->minute,
  70. rtc_time->second,
  71. rtc_time->day,
  72. rtc_time->month,
  73. rtc_time->year,
  74. rtc_time->weekday);
  75. return furi_string_alloc_set_str(json);
  76. }
  77. static bool update_save_rtc_time(DateTime *rtc_time)
  78. {
  79. if (!rtc_time)
  80. {
  81. FURI_LOG_E(TAG, "rtc_time is NULL");
  82. return false;
  83. }
  84. FuriString *json = update_datetime_to_json(rtc_time);
  85. if (!json)
  86. {
  87. FURI_LOG_E(TAG, "Failed to convert DateTime to JSON");
  88. return false;
  89. }
  90. save_char("last_checked", furi_string_get_cstr(json));
  91. furi_string_free(json);
  92. return true;
  93. }
  94. //
  95. // Returns true if time_current is one hour (or more) later than the stored last_checked time
  96. //
  97. static bool update_is_update_time(DateTime *time_current)
  98. {
  99. if (!time_current)
  100. {
  101. FURI_LOG_E(TAG, "time_current is NULL");
  102. return false;
  103. }
  104. char last_checked_old[128];
  105. if (!load_char("last_checked", last_checked_old, sizeof(last_checked_old)))
  106. {
  107. FURI_LOG_E(TAG, "Failed to load last_checked");
  108. FuriString *json = update_datetime_to_json(time_current);
  109. if (json)
  110. {
  111. save_char("last_checked", furi_string_get_cstr(json));
  112. furi_string_free(json);
  113. }
  114. return false;
  115. }
  116. DateTime last_updated_time;
  117. FuriString *last_updated_furi = char_to_furi_string(last_checked_old);
  118. if (!last_updated_furi)
  119. {
  120. FURI_LOG_E(TAG, "Failed to convert char to FuriString");
  121. return false;
  122. }
  123. if (!update_json_to_datetime(&last_updated_time, last_updated_furi))
  124. {
  125. FURI_LOG_E(TAG, "Failed to convert JSON to DateTime");
  126. furi_string_free(last_updated_furi);
  127. return false;
  128. }
  129. furi_string_free(last_updated_furi); // Free after usage.
  130. bool time_diff = false;
  131. // If the date is different assume more than one hour has passed.
  132. if (time_current->year != last_updated_time.year ||
  133. time_current->month != last_updated_time.month ||
  134. time_current->day != last_updated_time.day)
  135. {
  136. time_diff = true;
  137. }
  138. else
  139. {
  140. // For the same day, compute seconds from midnight.
  141. int seconds_current = time_current->hour * 3600 + time_current->minute * 60 + time_current->second;
  142. int seconds_last = last_updated_time.hour * 3600 + last_updated_time.minute * 60 + last_updated_time.second;
  143. if ((seconds_current - seconds_last) >= 3600)
  144. {
  145. time_diff = true;
  146. }
  147. }
  148. return time_diff;
  149. }
  150. // Sends a request to fetch the last updated date of the app.
  151. static bool update_last_app_update(FlipperHTTP *fhttp, bool flipper_server)
  152. {
  153. if (!fhttp)
  154. {
  155. FURI_LOG_E(TAG, "fhttp is NULL");
  156. return false;
  157. }
  158. char url[256];
  159. if (flipper_server)
  160. {
  161. // make sure folder is created
  162. char directory_path[256];
  163. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s", APP_ID);
  164. // Create the directory
  165. Storage *storage = furi_record_open(RECORD_STORAGE);
  166. storage_common_mkdir(storage, directory_path);
  167. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data", APP_ID);
  168. storage_common_mkdir(storage, directory_path);
  169. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data/last_update_request.txt", APP_ID);
  170. storage_simply_remove_recursive(storage, directory_path); // ensure the file is empty
  171. furi_record_close(RECORD_STORAGE);
  172. fhttp->save_received_data = false;
  173. fhttp->is_bytes_request = true;
  174. snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data/last_update_request.txt", APP_ID);
  175. snprintf(url, sizeof(url), "https://raw.githubusercontent.com/flipperdevices/flipper-application-catalog/main/applications/%s/%s/manifest.yml", APP_FOLDER, FAP_ID);
  176. return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\":\"application/json\"}", NULL);
  177. }
  178. else
  179. {
  180. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/last-updated/%s/", FAP_ID);
  181. return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
  182. }
  183. }
  184. static bool parse_yaml_version(const char *yaml, char *out_version, size_t out_len)
  185. {
  186. const char *p = strstr(yaml, "\nversion:");
  187. if (!p)
  188. {
  189. // maybe it's the very first line
  190. p = yaml;
  191. }
  192. else
  193. {
  194. // skip the “\n”
  195. p++;
  196. }
  197. // skip the key name and colon
  198. p = strstr(p, "version");
  199. if (!p)
  200. return false;
  201. p += strlen("version");
  202. // skip whitespace and colon
  203. while (*p == ' ' || *p == ':')
  204. p++;
  205. // handle optional quote
  206. bool quoted = (*p == '"');
  207. if (quoted)
  208. p++;
  209. // copy up until end‐quote or newline/space
  210. size_t i = 0;
  211. while (*p && i + 1 < out_len)
  212. {
  213. if ((quoted && *p == '"') ||
  214. (!quoted && (*p == '\n' || *p == ' ')))
  215. {
  216. break;
  217. }
  218. out_version[i++] = *p++;
  219. }
  220. out_version[i] = '\0';
  221. return (i > 0);
  222. }
  223. // Parses the server response and returns true if an update is available.
  224. static bool update_parse_last_app_update(FlipperHTTP *fhttp, DateTime *time_current, bool flipper_server)
  225. {
  226. if (!fhttp)
  227. {
  228. FURI_LOG_E(TAG, "fhttp is NULL");
  229. return false;
  230. }
  231. if (fhttp->state == ISSUE)
  232. {
  233. FURI_LOG_E(TAG, "Failed to fetch last app update");
  234. return false;
  235. }
  236. char version_str[32];
  237. if (!flipper_server)
  238. {
  239. if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
  240. {
  241. FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
  242. return false;
  243. }
  244. char *app_version = get_json_value("version", fhttp->last_response);
  245. if (app_version)
  246. {
  247. // Save the server app version: it should save something like: 0.8
  248. save_char("server_app_version", app_version);
  249. snprintf(version_str, sizeof(version_str), "%s", app_version);
  250. free(app_version);
  251. }
  252. else
  253. {
  254. FURI_LOG_E(TAG, "Failed to get app version");
  255. return false;
  256. }
  257. }
  258. else
  259. {
  260. FuriString *manifest_data = flipper_http_load_from_file(fhttp->file_path);
  261. if (!manifest_data)
  262. {
  263. FURI_LOG_E(TAG, "Failed to load app data");
  264. return false;
  265. }
  266. // parse version out of the YAML
  267. if (!parse_yaml_version(furi_string_get_cstr(manifest_data), version_str, sizeof(version_str)))
  268. {
  269. FURI_LOG_E(TAG, "Failed to parse version from YAML manifest");
  270. return false;
  271. }
  272. save_char("server_app_version", version_str);
  273. furi_string_free(manifest_data);
  274. }
  275. // Only check for an update if an hour or more has passed.
  276. if (update_is_update_time(time_current))
  277. {
  278. char app_version[32];
  279. if (!load_char("app_version", app_version, sizeof(app_version)))
  280. {
  281. FURI_LOG_E(TAG, "Failed to load app version");
  282. return false;
  283. }
  284. // Check if the app version is different from the server version.
  285. if (!update_is_str(app_version, version_str))
  286. {
  287. easy_flipper_dialog("Update available", "New update available!\nPress BACK to download.");
  288. return true; // Update available.
  289. }
  290. FURI_LOG_I(TAG, "No update available");
  291. return false; // No update available.
  292. }
  293. FURI_LOG_I(TAG, "Not enough time has passed since the last update check");
  294. return false; // Not yet time to update.
  295. }
  296. static bool update_get_fap_file(FlipperHTTP *fhttp, bool flipper_server)
  297. {
  298. if (!fhttp)
  299. {
  300. FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
  301. return false;
  302. }
  303. char url[256];
  304. fhttp->save_received_data = false;
  305. fhttp->is_bytes_request = true;
  306. #ifndef FW_ORIGIN_Momentum
  307. snprintf(
  308. fhttp->file_path,
  309. sizeof(fhttp->file_path),
  310. STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", APP_FOLDER, FAP_ID);
  311. #else
  312. if (strlen(MOM_FOLDER) == 0)
  313. snprintf(
  314. fhttp->file_path,
  315. sizeof(fhttp->file_path),
  316. STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", APP_FOLDER, FAP_ID);
  317. else
  318. snprintf(
  319. fhttp->file_path,
  320. sizeof(fhttp->file_path),
  321. STORAGE_EXT_PATH_PREFIX "/apps/%s/%s/%s.fap", APP_FOLDER, MOM_FOLDER, FAP_ID);
  322. #endif
  323. if (flipper_server)
  324. {
  325. uint8_t target;
  326. target = furi_hal_version_get_hw_target();
  327. uint16_t api_major, api_minor;
  328. furi_hal_info_get_api_version(&api_major, &api_minor);
  329. snprintf(
  330. url,
  331. sizeof(url),
  332. "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d",
  333. BUILD_ID,
  334. target,
  335. api_major,
  336. api_minor);
  337. }
  338. else
  339. {
  340. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/download/%s/", FAP_ID);
  341. }
  342. return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
  343. }
  344. // Updates the app. Uses the supplied current time for validating if update check should proceed.
  345. static bool update_update_app(FlipperHTTP *fhttp, DateTime *time_current, bool use_flipper_api)
  346. {
  347. if (!fhttp)
  348. {
  349. FURI_LOG_E(TAG, "fhttp is NULL");
  350. return false;
  351. }
  352. if (!update_last_app_update(fhttp, use_flipper_api))
  353. {
  354. FURI_LOG_E(TAG, "Failed to fetch last app update");
  355. return false;
  356. }
  357. fhttp->state = RECEIVING;
  358. furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS);
  359. while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
  360. {
  361. furi_delay_ms(100);
  362. }
  363. furi_timer_stop(fhttp->get_timeout_timer);
  364. if (update_parse_last_app_update(fhttp, time_current, use_flipper_api))
  365. {
  366. if (!update_get_fap_file(fhttp, false))
  367. {
  368. FURI_LOG_E(TAG, "Failed to fetch fap file 1");
  369. return false;
  370. }
  371. fhttp->state = RECEIVING;
  372. while (fhttp->state == RECEIVING)
  373. {
  374. furi_delay_ms(100);
  375. }
  376. if (fhttp->state == ISSUE)
  377. {
  378. FURI_LOG_E(TAG, "Failed to fetch fap file 2");
  379. easy_flipper_dialog("Update Error", "Failed to download the\nupdate file.\nPlease try again.");
  380. return false;
  381. }
  382. return true;
  383. }
  384. FURI_LOG_I(TAG, "No update available");
  385. return false; // No update available.
  386. }
  387. // Handles the app update routine. This function obtains the current RTC time,
  388. // checks the "last_checked" value, and if it is more than one hour old, calls for an update.
  389. bool update_is_ready(FlipperHTTP *fhttp, bool use_flipper_api)
  390. {
  391. if (!fhttp)
  392. {
  393. FURI_LOG_E(TAG, "fhttp is NULL");
  394. return false;
  395. }
  396. DateTime rtc_time;
  397. furi_hal_rtc_get_datetime(&rtc_time);
  398. char last_checked[32];
  399. if (!load_char("last_checked", last_checked, sizeof(last_checked)))
  400. {
  401. // First time – save the current time and check for an update.
  402. if (!update_save_rtc_time(&rtc_time))
  403. {
  404. FURI_LOG_E(TAG, "Failed to save RTC time");
  405. return false;
  406. }
  407. return update_update_app(fhttp, &rtc_time, use_flipper_api);
  408. }
  409. else
  410. {
  411. // Check if the current RTC time is at least one hour past the stored time.
  412. if (update_is_update_time(&rtc_time))
  413. {
  414. if (!update_update_app(fhttp, &rtc_time, use_flipper_api))
  415. {
  416. // save the last_checked for the next check.
  417. if (!update_save_rtc_time(&rtc_time))
  418. {
  419. FURI_LOG_E(TAG, "Failed to save RTC time");
  420. return false;
  421. }
  422. return false;
  423. }
  424. // Save the current time for the next check.
  425. if (!update_save_rtc_time(&rtc_time))
  426. {
  427. FURI_LOG_E(TAG, "Failed to save RTC time");
  428. return false;
  429. }
  430. return true;
  431. }
  432. return false; // No update necessary.
  433. }
  434. }