xremote_app.c 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. /*!
  2. * @file flipper-xremote/xremote_app.c
  3. @license This project is released under the GNU GPLv3 License
  4. * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com)
  5. *
  6. * @brief Shared functionality and data types between the apps.
  7. */
  8. #include "xremote_app.h"
  9. //////////////////////////////////////////////////////////////////////////////
  10. // XRemote generic functions and definitions
  11. //////////////////////////////////////////////////////////////////////////////
  12. #define XREMOTE_APP_SETTINGS APP_DATA_PATH("xremote.cfg")
  13. #define TAG "XRemoteApp"
  14. #define XREMOTE_ORIENTATION_TEXT_HORIZONTAL "Horizontal"
  15. #define XREMOTE_ORIENTATION_TEXT_VERTICAL "Vertical"
  16. #define XREMOTE_ORIENTATION_INDEX_HORIZONTAL 0
  17. #define XREMOTE_ORIENTATION_INDEX_VERTICAL 1
  18. #define XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS "Press"
  19. #define XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD "Hold"
  20. #define XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS 0
  21. #define XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD 1
  22. const NotificationSequence g_sequence_blink_purple_50 = {
  23. &message_red_255,
  24. &message_blue_255,
  25. &message_delay_50,
  26. NULL,
  27. };
  28. XRemoteAppExit xremote_app_get_exit_behavior(uint8_t exit_index) {
  29. return exit_index ? XRemoteAppExitHold : XRemoteAppExitPress;
  30. }
  31. ViewOrientation xremote_app_get_orientation(uint8_t orientation_index) {
  32. return orientation_index ? ViewOrientationVertical : ViewOrientationHorizontal;
  33. }
  34. const char* xremote_app_get_exit_str(XRemoteAppExit exit_behavior) {
  35. return exit_behavior == XRemoteAppExitPress ? "Press" : "Hold";
  36. }
  37. const char* xremote_app_get_orientation_str(ViewOrientation view_orientation) {
  38. return view_orientation == ViewOrientationHorizontal ? "Horizontal" : "Vertical";
  39. }
  40. uint32_t xremote_app_get_orientation_index(ViewOrientation view_orientation) {
  41. return view_orientation == ViewOrientationHorizontal ? 0 : 1;
  42. }
  43. uint32_t xremote_app_get_exit_index(XRemoteAppExit exit_behavior) {
  44. return exit_behavior == XRemoteAppExitPress ? 0 : 1;
  45. }
  46. void xremote_app_notification_blink(NotificationApp* notifications) {
  47. xremote_app_assert_void(notifications);
  48. notification_message(notifications, &g_sequence_blink_purple_50);
  49. }
  50. //////////////////////////////////////////////////////////////////////////////
  51. // XRemote buttons and custom button pairs
  52. //////////////////////////////////////////////////////////////////////////////
  53. bool xremote_app_extension_load(XRemoteAppButtons* buttons, FuriString* path) {
  54. Storage* storage = furi_record_open(RECORD_STORAGE);
  55. FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
  56. FuriString* tmp = furi_string_alloc();
  57. bool success = false;
  58. do {
  59. /* Open file and read the header */
  60. if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path))) break;
  61. if(!flipper_format_read_string(ff, "custom_ok", tmp)) break;
  62. furi_string_set(buttons->custom_ok, tmp);
  63. if(!flipper_format_read_string(ff, "custom_up", tmp)) break;
  64. furi_string_set(buttons->custom_up, tmp);
  65. if(!flipper_format_read_string(ff, "custom_down", tmp)) break;
  66. furi_string_set(buttons->custom_down, tmp);
  67. if(!flipper_format_read_string(ff, "custom_left", tmp)) break;
  68. furi_string_set(buttons->custom_left, tmp);
  69. if(!flipper_format_read_string(ff, "custom_right", tmp)) break;
  70. furi_string_set(buttons->custom_right, tmp);
  71. if(!flipper_format_read_string(ff, "custom_ok_hold", tmp)) break;
  72. furi_string_set(buttons->custom_ok_hold, tmp);
  73. if(!flipper_format_read_string(ff, "custom_up_hold", tmp)) break;
  74. furi_string_set(buttons->custom_up_hold, tmp);
  75. if(!flipper_format_read_string(ff, "custom_down_hold", tmp)) break;
  76. furi_string_set(buttons->custom_down_hold, tmp);
  77. if(!flipper_format_read_string(ff, "custom_left_hold", tmp)) break;
  78. furi_string_set(buttons->custom_left_hold, tmp);
  79. if(!flipper_format_read_string(ff, "custom_right_hold", tmp)) break;
  80. furi_string_set(buttons->custom_right_hold, tmp);
  81. success = true;
  82. } while(false);
  83. furi_record_close(RECORD_STORAGE);
  84. flipper_format_free(ff);
  85. furi_string_free(tmp);
  86. return success;
  87. }
  88. bool xremote_app_extension_store(XRemoteAppButtons* buttons, FuriString* path) {
  89. Storage* storage = furi_record_open(RECORD_STORAGE);
  90. FlipperFormat* ff = flipper_format_file_alloc(storage);
  91. bool success = false;
  92. do {
  93. if(!flipper_format_file_open_append(ff, furi_string_get_cstr(path))) break;
  94. if(!flipper_format_write_comment_cstr(ff, "XRemote extension")) break;
  95. if(!flipper_format_write_string(ff, "custom_ok", buttons->custom_ok)) break;
  96. if(!flipper_format_write_string(ff, "custom_up", buttons->custom_up)) break;
  97. if(!flipper_format_write_string(ff, "custom_down", buttons->custom_down)) break;
  98. if(!flipper_format_write_string(ff, "custom_left", buttons->custom_left)) break;
  99. if(!flipper_format_write_string(ff, "custom_right", buttons->custom_right)) break;
  100. if(!flipper_format_write_string(ff, "custom_ok_hold", buttons->custom_ok_hold)) break;
  101. if(!flipper_format_write_string(ff, "custom_up_hold", buttons->custom_up_hold)) break;
  102. if(!flipper_format_write_string(ff, "custom_down_hold", buttons->custom_down_hold)) break;
  103. if(!flipper_format_write_string(ff, "custom_left_hold", buttons->custom_left_hold)) break;
  104. if(!flipper_format_write_string(ff, "custom_right_hold", buttons->custom_right_hold))
  105. break;
  106. success = true;
  107. } while(false);
  108. furi_record_close(RECORD_STORAGE);
  109. flipper_format_free(ff);
  110. return success;
  111. }
  112. void xremote_app_buttons_free(XRemoteAppButtons* buttons) {
  113. xremote_app_assert_void(buttons);
  114. infrared_remote_free(buttons->remote);
  115. furi_string_free(buttons->custom_up);
  116. furi_string_free(buttons->custom_down);
  117. furi_string_free(buttons->custom_left);
  118. furi_string_free(buttons->custom_right);
  119. furi_string_free(buttons->custom_ok);
  120. furi_string_free(buttons->custom_up_hold);
  121. furi_string_free(buttons->custom_down_hold);
  122. furi_string_free(buttons->custom_left_hold);
  123. furi_string_free(buttons->custom_right_hold);
  124. furi_string_free(buttons->custom_ok_hold);
  125. free(buttons);
  126. }
  127. XRemoteAppButtons* xremote_app_buttons_alloc() {
  128. XRemoteAppButtons* buttons = malloc(sizeof(XRemoteAppButtons));
  129. buttons->remote = infrared_remote_alloc();
  130. buttons->app_ctx = NULL;
  131. /* Setup default buttons for custom layout */
  132. buttons->custom_up = furi_string_alloc_set_str(XREMOTE_COMMAND_UP);
  133. buttons->custom_down = furi_string_alloc_set_str(XREMOTE_COMMAND_DOWN);
  134. buttons->custom_left = furi_string_alloc_set_str(XREMOTE_COMMAND_LEFT);
  135. buttons->custom_right = furi_string_alloc_set_str(XREMOTE_COMMAND_RIGHT);
  136. buttons->custom_ok = furi_string_alloc_set_str(XREMOTE_COMMAND_OK);
  137. buttons->custom_up_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_INPUT);
  138. buttons->custom_down_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_SETUP);
  139. buttons->custom_left_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_MENU);
  140. buttons->custom_right_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_LIST);
  141. buttons->custom_ok_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_POWER);
  142. return buttons;
  143. }
  144. XRemoteAppButtons* xremote_app_buttons_load(XRemoteAppContext* app_ctx) {
  145. /* Show file selection dialog (returns selected file path with app_ctx->file_path) */
  146. if(!xremote_app_browser_select_file(app_ctx, XREMOTE_APP_EXTENSION)) return NULL;
  147. XRemoteAppButtons* buttons = xremote_app_buttons_alloc();
  148. buttons->app_ctx = app_ctx;
  149. /* Load buttons from the selected path */
  150. if(!infrared_remote_load(buttons->remote, app_ctx->file_path)) {
  151. xremote_app_buttons_free(buttons);
  152. return NULL;
  153. }
  154. /* Load custom buttons from the selected path */
  155. xremote_app_extension_load(buttons, app_ctx->file_path);
  156. return buttons;
  157. }
  158. //////////////////////////////////////////////////////////////////////////////
  159. // XRemote application settings
  160. //////////////////////////////////////////////////////////////////////////////
  161. XRemoteAppSettings* xremote_app_settings_alloc() {
  162. XRemoteAppSettings* settings = malloc(sizeof(XRemoteAppSettings));
  163. settings->orientation = ViewOrientationHorizontal;
  164. settings->exit_behavior = XRemoteAppExitPress;
  165. settings->repeat_count = 2;
  166. return settings;
  167. }
  168. void xremote_app_settings_free(XRemoteAppSettings* settings) {
  169. xremote_app_assert_void(settings);
  170. free(settings);
  171. }
  172. bool xremote_app_settings_store(XRemoteAppSettings* settings) {
  173. Storage* storage = furi_record_open(RECORD_STORAGE);
  174. FlipperFormat* ff = flipper_format_file_alloc(storage);
  175. FURI_LOG_I(TAG, "store config file: \'%s\'", XREMOTE_APP_SETTINGS);
  176. bool success = false;
  177. do {
  178. /* Write header in config file */
  179. if(!flipper_format_file_open_always(ff, XREMOTE_APP_SETTINGS)) break;
  180. if(!flipper_format_write_header_cstr(ff, "XRemote", 1)) break;
  181. if(!flipper_format_write_comment_cstr(ff, "")) break;
  182. /* Write actual configuration to the settings file */
  183. uint32_t value = xremote_app_get_orientation_index(settings->orientation);
  184. if(!flipper_format_write_uint32(ff, "orientation", &value, 1)) break;
  185. value = xremote_app_get_exit_index(settings->exit_behavior);
  186. if(!flipper_format_write_uint32(ff, "appexit", &value, 1)) break;
  187. value = settings->repeat_count;
  188. if(!flipper_format_write_uint32(ff, "repeat", &value, 1)) break;
  189. success = true;
  190. } while(false);
  191. furi_record_close(RECORD_STORAGE);
  192. flipper_format_free(ff);
  193. return success;
  194. }
  195. bool xremote_app_settings_load(XRemoteAppSettings* settings) {
  196. Storage* storage = furi_record_open(RECORD_STORAGE);
  197. FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
  198. FuriString* header = furi_string_alloc();
  199. FURI_LOG_I(TAG, "load config file: \'%s\'", XREMOTE_APP_SETTINGS);
  200. uint32_t version = 0;
  201. uint32_t value = 0;
  202. bool success = false;
  203. do {
  204. /* Open file and read the header */
  205. if(!flipper_format_buffered_file_open_existing(ff, XREMOTE_APP_SETTINGS)) break;
  206. if(!flipper_format_read_header(ff, header, &version)) break;
  207. if(!furi_string_equal(header, "XRemote") || (version != 1)) break;
  208. /* Parse config data from the buffer */
  209. if(!flipper_format_read_uint32(ff, "orientation", &value, 1)) break;
  210. settings->orientation = xremote_app_get_orientation(value);
  211. if(!flipper_format_read_uint32(ff, "appexit", &value, 1)) break;
  212. settings->exit_behavior = xremote_app_get_exit_behavior(value);
  213. if(!flipper_format_read_uint32(ff, "repeat", &value, 1)) break;
  214. settings->repeat_count = value;
  215. success = true;
  216. } while(false);
  217. furi_record_close(RECORD_STORAGE);
  218. furi_string_free(header);
  219. flipper_format_free(ff);
  220. return success;
  221. }
  222. //////////////////////////////////////////////////////////////////////////////
  223. // XRemote gloal context shared between every child application
  224. //////////////////////////////////////////////////////////////////////////////
  225. XRemoteAppContext* xremote_app_context_alloc(void* arg) {
  226. XRemoteAppContext* ctx = malloc(sizeof(XRemoteAppContext));
  227. ctx->app_argument = arg;
  228. ctx->file_path = NULL;
  229. /* Open GUI and norification records */
  230. ctx->gui = furi_record_open(RECORD_GUI);
  231. ctx->notifications = furi_record_open(RECORD_NOTIFICATION);
  232. /* Allocate and load global app settings */
  233. ctx->app_settings = xremote_app_settings_alloc();
  234. xremote_app_settings_load(ctx->app_settings);
  235. /* Allocate and setup view dispatcher */
  236. ctx->view_dispatcher = view_dispatcher_alloc();
  237. view_dispatcher_enable_queue(ctx->view_dispatcher);
  238. view_dispatcher_attach_to_gui(ctx->view_dispatcher, ctx->gui, ViewDispatcherTypeFullscreen);
  239. return ctx;
  240. }
  241. void xremote_app_context_free(XRemoteAppContext* ctx) {
  242. xremote_app_assert_void(ctx);
  243. notification_internal_message(ctx->notifications, &sequence_reset_blue);
  244. xremote_app_settings_free(ctx->app_settings);
  245. view_dispatcher_free(ctx->view_dispatcher);
  246. furi_record_close(RECORD_NOTIFICATION);
  247. furi_record_close(RECORD_GUI);
  248. if(ctx->file_path != NULL) {
  249. furi_string_free(ctx->file_path);
  250. ctx->file_path = NULL;
  251. }
  252. free(ctx);
  253. }
  254. bool xremote_app_browser_select_file(XRemoteAppContext* app_ctx, const char* extension) {
  255. DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
  256. Storage* storage = furi_record_open(RECORD_STORAGE);
  257. storage_simply_mkdir(storage, XREMOTE_APP_FOLDER);
  258. if(app_ctx->file_path == NULL) {
  259. app_ctx->file_path = furi_string_alloc();
  260. furi_string_set(app_ctx->file_path, XREMOTE_APP_FOLDER);
  261. }
  262. /* Open file browser (view and dialogs are managed by the browser itself) */
  263. DialogsFileBrowserOptions browser;
  264. dialog_file_browser_set_basic_options(&browser, extension, &I_IR_Icon_10x10);
  265. browser.base_path = XREMOTE_APP_FOLDER;
  266. FuriString* path = app_ctx->file_path;
  267. /* Show file selection dialog (returns selected file path with file_path) */
  268. bool status = dialog_file_browser_show(dialogs, path, path, &browser);
  269. /* Cleanup file loading context */
  270. furi_record_close(RECORD_STORAGE);
  271. furi_record_close(RECORD_DIALOGS);
  272. return status;
  273. }
  274. const char* xremote_app_context_get_exit_str(XRemoteAppContext* app_ctx) {
  275. XRemoteAppExit exit_behavior = app_ctx->app_settings->exit_behavior;
  276. return exit_behavior == XRemoteAppExitHold ? "Hold to exit" : "Press to exit";
  277. }
  278. void xremote_app_context_notify_led(XRemoteAppContext* app_ctx) {
  279. xremote_app_assert_void(app_ctx);
  280. xremote_app_notification_blink(app_ctx->notifications);
  281. }
  282. bool xremote_app_send_signal(XRemoteAppContext* app_ctx, InfraredSignal* signal) {
  283. xremote_app_assert(signal, false);
  284. XRemoteAppSettings* settings = app_ctx->app_settings;
  285. infrared_signal_transmit_times(signal, settings->repeat_count);
  286. xremote_app_context_notify_led(app_ctx);
  287. return true;
  288. }
  289. //////////////////////////////////////////////////////////////////////////////
  290. // XRemote application factory
  291. //////////////////////////////////////////////////////////////////////////////
  292. void xremote_app_view_alloc(XRemoteApp* app, uint32_t view_id, XRemoteViewAllocator allocator) {
  293. furi_assert(app);
  294. xremote_app_assert_void(app->app_ctx);
  295. if(app->view_id == view_id && app->view_ctx != NULL) return;
  296. xremote_app_view_free(app);
  297. app->view_id = view_id;
  298. app->view_ctx = allocator(app->app_ctx);
  299. View* app_view = xremote_view_get_view(app->view_ctx);
  300. ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
  301. view_dispatcher_add_view(view_disp, app->view_id, app_view);
  302. }
  303. void xremote_app_view_alloc2(
  304. XRemoteApp* app,
  305. uint32_t view_id,
  306. XRemoteViewAllocator2 allocator,
  307. void* model_ctx) {
  308. furi_assert(app);
  309. xremote_app_assert_void(app->app_ctx);
  310. if(app->view_id == view_id && app->view_ctx != NULL) return;
  311. xremote_app_view_free(app);
  312. app->view_id = view_id;
  313. app->view_ctx = allocator(app->app_ctx, model_ctx);
  314. View* app_view = xremote_view_get_view(app->view_ctx);
  315. ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
  316. view_dispatcher_add_view(view_disp, app->view_id, app_view);
  317. }
  318. void xremote_app_view_free(XRemoteApp* app) {
  319. xremote_app_assert_void(app);
  320. if(app->app_ctx != NULL && app->view_id != XRemoteViewNone) {
  321. ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
  322. view_dispatcher_remove_view(view_disp, app->view_id);
  323. app->view_id = XRemoteViewNone;
  324. }
  325. if(app->view_ctx != NULL) {
  326. xremote_view_free(app->view_ctx);
  327. app->view_ctx = NULL;
  328. }
  329. }
  330. bool xremote_app_has_view(XRemoteApp* app, uint32_t view_id) {
  331. xremote_app_assert(app, false);
  332. return (app->view_id == view_id || app->submenu_id == view_id);
  333. }
  334. void xremote_app_switch_to_view(XRemoteApp* app, uint32_t view_id) {
  335. furi_assert(app);
  336. xremote_app_assert_void(app->app_ctx);
  337. ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
  338. view_dispatcher_switch_to_view(view_disp, view_id);
  339. }
  340. void xremote_app_switch_to_submenu(XRemoteApp* app) {
  341. furi_assert(app);
  342. xremote_app_assert_void(app->app_ctx);
  343. ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
  344. view_dispatcher_switch_to_view(view_disp, app->submenu_id);
  345. }
  346. void xremote_app_submenu_add(
  347. XRemoteApp* app,
  348. const char* name,
  349. uint32_t index,
  350. SubmenuItemCallback callback) {
  351. furi_assert(app);
  352. xremote_app_assert_void(app->submenu);
  353. submenu_add_item(app->submenu, name, index, callback, app);
  354. }
  355. void xremote_app_submenu_alloc(XRemoteApp* app, uint32_t index, ViewNavigationCallback prev_cb) {
  356. furi_assert(app);
  357. app->submenu = submenu_alloc();
  358. app->submenu_id = index;
  359. XRemoteAppSettings* settings = app->app_ctx->app_settings;
  360. View* view = submenu_get_view(app->submenu);
  361. view_set_previous_callback(view, prev_cb);
  362. #if defined(FW_ORIGIN_Unleashed) || defined(FW_ORIGIN_RM)
  363. submenu_set_orientation(app->submenu, settings->orientation);
  364. #else
  365. view_set_orientation(view, settings->orientation);
  366. #endif
  367. ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
  368. view_dispatcher_add_view(view_disp, app->submenu_id, view);
  369. }
  370. void xremote_app_submenu_free(XRemoteApp* app) {
  371. xremote_app_assert_void(app);
  372. /* Remove submenu view from dispatcher */
  373. if(app->submenu_id != XRemoteViewNone && app->app_ctx != NULL) {
  374. ViewDispatcher* view_disp = app->app_ctx->view_dispatcher;
  375. view_dispatcher_remove_view(view_disp, app->submenu_id);
  376. app->submenu_id = XRemoteViewNone;
  377. }
  378. /* Free submenu */
  379. if(app->submenu != NULL) {
  380. submenu_free(app->submenu);
  381. app->submenu = NULL;
  382. }
  383. }
  384. void xremote_app_view_set_previous_callback(XRemoteApp* app, ViewNavigationCallback callback) {
  385. furi_assert(app);
  386. xremote_app_assert_void(app->view_ctx);
  387. View* view = xremote_view_get_view(app->view_ctx);
  388. view_set_previous_callback(view, callback);
  389. }
  390. void xremote_app_set_view_context(XRemoteApp* app, void* context, XRemoteClearCallback on_clear) {
  391. furi_assert(app);
  392. xremote_app_assert_void(app->view_ctx);
  393. xremote_view_set_context(app->view_ctx, context, on_clear);
  394. }
  395. void xremote_app_set_user_context(XRemoteApp* app, void* context, XRemoteClearCallback on_clear) {
  396. furi_assert(app);
  397. app->on_clear = on_clear;
  398. app->context = context;
  399. }
  400. void xremote_app_user_context_free(XRemoteApp* app) {
  401. furi_assert(app);
  402. xremote_app_assert_void(app->context);
  403. xremote_app_assert_void(app->on_clear);
  404. app->on_clear(app->context);
  405. app->on_clear = NULL;
  406. app->context = NULL;
  407. }
  408. XRemoteApp* xremote_app_alloc(XRemoteAppContext* ctx) {
  409. furi_assert(ctx);
  410. XRemoteApp* app = malloc(sizeof(XRemoteApp));
  411. xremote_app_assert(app, NULL);
  412. app->submenu_id = XRemoteViewNone;
  413. app->view_id = XRemoteViewNone;
  414. app->app_ctx = ctx;
  415. app->submenu = NULL;
  416. app->view_ctx = NULL;
  417. app->on_clear = NULL;
  418. app->context = NULL;
  419. return app;
  420. }
  421. void xremote_app_free(XRemoteApp* app) {
  422. xremote_app_assert_void(app);
  423. xremote_app_submenu_free(app);
  424. xremote_app_view_free(app);
  425. /* Call clear callback if there is an user context attached */
  426. if(app->on_clear != NULL) app->on_clear(app->context);
  427. free(app);
  428. }