flip_store_apps.c 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. #include <apps/flip_store_apps.h>
  2. FlipStoreAppInfo *flip_catalog = NULL;
  3. uint32_t app_selected_index = 0;
  4. bool flip_store_sent_request = false;
  5. bool flip_store_success = false;
  6. bool flip_store_saved_data = false;
  7. bool flip_store_saved_success = false;
  8. uint32_t flip_store_category_index = 0;
  9. FlipStoreAppInfo *flip_catalog_alloc()
  10. {
  11. FlipStoreAppInfo *app_catalog = (FlipStoreAppInfo *)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo));
  12. if (!app_catalog)
  13. {
  14. FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog.");
  15. return NULL;
  16. }
  17. for (int i = 0; i < MAX_APP_COUNT; i++)
  18. {
  19. app_catalog[i].app_name = (char *)malloc(MAX_APP_NAME_LENGTH * sizeof(char));
  20. if (!app_catalog[i].app_name)
  21. {
  22. FURI_LOG_E(TAG, "Failed to allocate memory for app_name.");
  23. return NULL;
  24. }
  25. app_catalog[i].app_id = (char *)malloc(MAX_APP_NAME_LENGTH * sizeof(char));
  26. if (!app_catalog[i].app_id)
  27. {
  28. FURI_LOG_E(TAG, "Failed to allocate memory for app_id.");
  29. return NULL;
  30. }
  31. app_catalog[i].app_build_id = (char *)malloc(MAX_ID_LENGTH * sizeof(char));
  32. if (!app_catalog[i].app_build_id)
  33. {
  34. FURI_LOG_E(TAG, "Failed to allocate memory for app_build_id.");
  35. return NULL;
  36. }
  37. }
  38. return app_catalog;
  39. }
  40. void flip_catalog_free()
  41. {
  42. if (!flip_catalog)
  43. {
  44. return;
  45. }
  46. for (int i = 0; i < MAX_APP_COUNT; i++)
  47. {
  48. if (flip_catalog[i].app_name)
  49. {
  50. free(flip_catalog[i].app_name);
  51. }
  52. if (flip_catalog[i].app_id)
  53. {
  54. free(flip_catalog[i].app_id);
  55. }
  56. if (flip_catalog[i].app_build_id)
  57. {
  58. free(flip_catalog[i].app_build_id);
  59. }
  60. }
  61. }
  62. // Utility function to parse JSON incrementally from a file
  63. bool flip_store_process_app_list()
  64. {
  65. // initialize the flip_catalog
  66. flip_catalog = flip_catalog_alloc();
  67. if (!flip_catalog)
  68. {
  69. FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog.");
  70. return false;
  71. }
  72. Storage *_storage = NULL;
  73. File *_file = NULL;
  74. char buffer[BUFFER_SIZE];
  75. size_t bytes_read;
  76. bool in_string = false;
  77. bool is_escaped = false;
  78. bool reading_key = false;
  79. bool reading_value = false;
  80. bool inside_app_object = false;
  81. bool found_name = false;
  82. bool found_id = false;
  83. bool found_build_id = false;
  84. char current_key[MAX_KEY_LENGTH] = {0};
  85. size_t key_index = 0;
  86. char current_value[MAX_VALUE_LENGTH] = {0};
  87. size_t value_index = 0;
  88. int app_count = 0;
  89. enum ObjectState object_state = OBJECT_EXPECT_KEY; // Initialize object_state
  90. // Initialize parser state
  91. enum
  92. {
  93. STATE_SEARCH_APPS_KEY,
  94. STATE_SEARCH_ARRAY_START,
  95. STATE_READ_ARRAY_ELEMENTS,
  96. STATE_DONE
  97. } state = STATE_SEARCH_APPS_KEY;
  98. // Open storage and file
  99. _storage = furi_record_open(RECORD_STORAGE);
  100. if (!_storage)
  101. {
  102. FURI_LOG_E(TAG, "Failed to open storage.");
  103. return false;
  104. }
  105. _file = storage_file_alloc(_storage);
  106. if (!_file)
  107. {
  108. FURI_LOG_E(TAG, "Failed to allocate file.");
  109. furi_record_close(RECORD_STORAGE);
  110. return false;
  111. }
  112. if (!storage_file_open(_file, fhttp.file_path, FSAM_READ, FSOM_OPEN_EXISTING))
  113. {
  114. FURI_LOG_E(TAG, "Failed to open JSON file for reading.");
  115. storage_file_free(_file);
  116. furi_record_close(RECORD_STORAGE);
  117. return false;
  118. }
  119. while ((bytes_read = storage_file_read(_file, buffer, BUFFER_SIZE)) > 0 && state != STATE_DONE)
  120. {
  121. for (size_t i = 0; i < bytes_read; ++i)
  122. {
  123. char c = buffer[i];
  124. if (is_escaped)
  125. {
  126. is_escaped = false;
  127. if (reading_key)
  128. {
  129. if (key_index < MAX_KEY_LENGTH - 1)
  130. {
  131. current_key[key_index++] = c;
  132. }
  133. }
  134. else if (reading_value)
  135. {
  136. if (value_index < MAX_VALUE_LENGTH - 1)
  137. {
  138. current_value[value_index++] = c;
  139. }
  140. }
  141. continue;
  142. }
  143. if (c == '\\')
  144. {
  145. is_escaped = true;
  146. continue;
  147. }
  148. if (c == '\"')
  149. {
  150. in_string = !in_string;
  151. if (in_string)
  152. {
  153. // Start of a string
  154. if (!reading_key && !reading_value)
  155. {
  156. if (state == STATE_SEARCH_APPS_KEY)
  157. {
  158. reading_key = true;
  159. key_index = 0;
  160. current_key[0] = '\0';
  161. }
  162. else if (inside_app_object)
  163. {
  164. if (object_state == OBJECT_EXPECT_KEY)
  165. {
  166. reading_key = true;
  167. key_index = 0;
  168. current_key[0] = '\0';
  169. }
  170. else if (object_state == OBJECT_EXPECT_VALUE)
  171. {
  172. reading_value = true;
  173. value_index = 0;
  174. current_value[0] = '\0';
  175. }
  176. }
  177. }
  178. }
  179. else
  180. {
  181. // End of a string
  182. if (reading_key)
  183. {
  184. reading_key = false;
  185. current_key[key_index] = '\0';
  186. if (state == STATE_SEARCH_APPS_KEY && strcmp(current_key, "apps") == 0)
  187. {
  188. state = STATE_SEARCH_ARRAY_START;
  189. }
  190. else if (inside_app_object)
  191. {
  192. object_state = OBJECT_EXPECT_COLON;
  193. }
  194. }
  195. else if (reading_value)
  196. {
  197. reading_value = false;
  198. current_value[value_index] = '\0';
  199. if (inside_app_object)
  200. {
  201. if (strcmp(current_key, "name") == 0)
  202. {
  203. snprintf(flip_catalog[app_count].app_name, MAX_APP_NAME_LENGTH, "%.31s", current_value);
  204. found_name = true;
  205. }
  206. else if (strcmp(current_key, "id") == 0)
  207. {
  208. snprintf(flip_catalog[app_count].app_id, MAX_ID_LENGTH, "%.31s", current_value);
  209. found_id = true;
  210. }
  211. else if (strcmp(current_key, "build_id") == 0)
  212. {
  213. snprintf(flip_catalog[app_count].app_build_id, MAX_ID_LENGTH, "%.31s", current_value);
  214. found_build_id = true;
  215. }
  216. // After processing value, expect comma or end
  217. object_state = OBJECT_EXPECT_COMMA_OR_END;
  218. // Check if both name and id are found
  219. if (found_name && found_id && found_build_id)
  220. {
  221. app_count++;
  222. if (app_count >= MAX_APP_COUNT)
  223. {
  224. FURI_LOG_I(TAG, "Reached maximum app count.");
  225. state = STATE_DONE;
  226. break;
  227. }
  228. // Reset for next app
  229. found_name = false;
  230. found_id = false;
  231. found_build_id = false;
  232. }
  233. }
  234. }
  235. }
  236. continue;
  237. }
  238. if (in_string)
  239. {
  240. if (reading_key)
  241. {
  242. if (key_index < MAX_KEY_LENGTH - 1)
  243. {
  244. current_key[key_index++] = c;
  245. }
  246. }
  247. else if (reading_value)
  248. {
  249. if (value_index < MAX_VALUE_LENGTH - 1)
  250. {
  251. current_value[value_index++] = c;
  252. }
  253. }
  254. continue;
  255. }
  256. // Not inside a string
  257. if (state == STATE_SEARCH_ARRAY_START)
  258. {
  259. if (c == '[')
  260. {
  261. state = STATE_READ_ARRAY_ELEMENTS;
  262. }
  263. continue;
  264. }
  265. if (state == STATE_READ_ARRAY_ELEMENTS)
  266. {
  267. if (c == '{')
  268. {
  269. inside_app_object = true;
  270. found_name = false;
  271. found_id = false;
  272. found_build_id = false;
  273. object_state = OBJECT_EXPECT_KEY;
  274. }
  275. else if (c == '}')
  276. {
  277. inside_app_object = false;
  278. object_state = OBJECT_EXPECT_KEY;
  279. }
  280. else if (c == ']')
  281. {
  282. state = STATE_DONE;
  283. break;
  284. }
  285. else if (c == ':')
  286. {
  287. if (inside_app_object && object_state == OBJECT_EXPECT_COLON)
  288. {
  289. object_state = OBJECT_EXPECT_VALUE;
  290. }
  291. }
  292. else if (c == ',')
  293. {
  294. if (inside_app_object && object_state == OBJECT_EXPECT_COMMA_OR_END)
  295. {
  296. object_state = OBJECT_EXPECT_KEY;
  297. }
  298. // Else, separator between objects or values
  299. }
  300. // Ignore other characters like whitespace, etc.
  301. continue;
  302. }
  303. }
  304. }
  305. // Clean up
  306. storage_file_close(_file);
  307. storage_file_free(_file);
  308. furi_record_close(RECORD_STORAGE);
  309. return app_count > 0;
  310. }
  311. bool flip_store_get_fap_file(char *build_id, uint8_t target, uint16_t api_major, uint16_t api_minor)
  312. {
  313. char url[128];
  314. fhttp.save_received_data = false;
  315. fhttp.is_bytes_request = true;
  316. snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d", build_id, target, api_major, api_minor);
  317. char *headers = jsmn("Content-Type", "application/octet-stream");
  318. bool sent_request = flipper_http_get_request_bytes(url, headers);
  319. free(headers);
  320. return sent_request;
  321. }
  322. void flip_store_request_error(Canvas *canvas)
  323. {
  324. if (fhttp.last_response != NULL)
  325. {
  326. if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
  327. {
  328. canvas_clear(canvas);
  329. canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
  330. canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
  331. canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
  332. }
  333. else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
  334. {
  335. canvas_clear(canvas);
  336. canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
  337. canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
  338. canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
  339. }
  340. else
  341. {
  342. FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response);
  343. canvas_draw_str(canvas, 0, 42, "Unusual error...");
  344. canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
  345. canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
  346. }
  347. }
  348. else
  349. {
  350. canvas_clear(canvas);
  351. canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error.");
  352. canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
  353. canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
  354. }
  355. }
  356. // function to handle the entire installation process "asynchronously"
  357. bool flip_store_install_app(Canvas *canvas, char *category)
  358. {
  359. // create /apps/FlipStore directory if it doesn't exist
  360. char directory_path[128];
  361. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s", category);
  362. // Create the directory
  363. Storage *storage = furi_record_open(RECORD_STORAGE);
  364. storage_common_mkdir(storage, directory_path);
  365. // Adjusted to access flip_catalog as an array of structures
  366. char installing_text[64];
  367. snprintf(installing_text, sizeof(installing_text), "Installing %s", flip_catalog[app_selected_index].app_name);
  368. snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id);
  369. canvas_draw_str(canvas, 0, 10, installing_text);
  370. canvas_draw_str(canvas, 0, 20, "Sending request..");
  371. uint8_t target = furi_hal_version_get_hw_target();
  372. uint16_t api_major, api_minor;
  373. furi_hal_info_get_api_version(&api_major, &api_minor);
  374. if (fhttp.state != INACTIVE && flip_store_get_fap_file(flip_catalog[app_selected_index].app_build_id, target, api_major, api_minor))
  375. {
  376. canvas_draw_str(canvas, 0, 30, "Request sent.");
  377. fhttp.state = RECEIVING;
  378. // furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
  379. canvas_draw_str(canvas, 0, 40, "Receiving...");
  380. }
  381. else
  382. {
  383. FURI_LOG_E(TAG, "Failed to send the request");
  384. flip_store_success = false;
  385. return false;
  386. }
  387. while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
  388. {
  389. // Wait for the feed to be received
  390. furi_delay_ms(10);
  391. }
  392. // furi_timer_stop(fhttp.get_timeout_timer);
  393. if (fhttp.state == ISSUE)
  394. {
  395. flip_store_request_error(canvas);
  396. flip_store_success = false;
  397. return false;
  398. }
  399. flip_store_success = true;
  400. return true;
  401. }
  402. // process the app list and return view
  403. int32_t flip_store_handle_app_list(FlipStoreApp *app, int32_t success_view, char *category, Submenu **submenu)
  404. {
  405. // reset the flip_catalog
  406. flip_catalog_free();
  407. if (!app)
  408. {
  409. FURI_LOG_E(TAG, "FlipStoreApp is NULL");
  410. return FlipStoreViewPopup;
  411. }
  412. char url[128];
  413. snprintf(
  414. fhttp.file_path,
  415. sizeof(fhttp.file_path),
  416. STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/apps.txt");
  417. fhttp.save_received_data = true;
  418. fhttp.is_bytes_request = false;
  419. snprintf(url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/extended/", category);
  420. char *headers = jsmn("Content-Type", "application/json");
  421. // async call to the app list with timer
  422. if (fhttp.state != INACTIVE && flipper_http_get_request_with_headers(url, headers))
  423. {
  424. furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
  425. free(headers);
  426. fhttp.state = RECEIVING;
  427. }
  428. else
  429. {
  430. FURI_LOG_E(TAG, "Failed to send the request");
  431. free(headers);
  432. fhttp.state = ISSUE;
  433. return FlipStoreViewPopup;
  434. }
  435. while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
  436. {
  437. // Wait for the feed to be received
  438. furi_delay_ms(100);
  439. }
  440. furi_timer_stop(fhttp.get_timeout_timer);
  441. if (fhttp.state == ISSUE)
  442. {
  443. FURI_LOG_E(TAG, "Failed to receive data");
  444. if (fhttp.last_response == NULL)
  445. {
  446. if (fhttp.last_response != NULL)
  447. {
  448. if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
  449. {
  450. popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
  451. }
  452. else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
  453. {
  454. popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
  455. }
  456. else
  457. {
  458. popup_set_text(app->popup, fhttp.last_response, 0, 10, AlignLeft, AlignTop);
  459. }
  460. }
  461. else
  462. {
  463. popup_set_text(app->popup, "[ERROR] Unknown Error.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
  464. }
  465. return FlipStoreViewPopup;
  466. }
  467. else
  468. {
  469. popup_set_text(app->popup, "Failed to received data.", 0, 10, AlignLeft, AlignTop);
  470. return FlipStoreViewPopup;
  471. }
  472. }
  473. else
  474. {
  475. // process the app list
  476. if (flip_store_process_app_list())
  477. {
  478. submenu_reset(*submenu);
  479. // add each app name to submenu
  480. for (int i = 0; i < MAX_APP_COUNT; i++)
  481. {
  482. if (strlen(flip_catalog[i].app_name) > 0)
  483. {
  484. submenu_add_item(*submenu, flip_catalog[i].app_name, FlipStoreSubmenuIndexStartAppList + i, callback_submenu_choices, app);
  485. }
  486. }
  487. return success_view;
  488. }
  489. else
  490. {
  491. FURI_LOG_E(TAG, "Failed to process the app list");
  492. popup_set_text(app->popup, "Failed to process the app list", 0, 10, AlignLeft, AlignTop);
  493. return FlipStoreViewPopup;
  494. }
  495. }
  496. }