flip_store_apps.c 18 KB

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