From 582797aa4c250a80e739567a8a3835642a88ecf5 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Thu, 15 Oct 2026 19:28:36 +0200 Subject: [PATCH] App Hub: update-all, commandline app, better progress indication --- .../fonts/MaterialSymbolsRounded.codepoints | 1 + Data/system/fonts/MaterialSymbolsRounded.ttf | Bin 20484 -> 20700 bytes Documentation/ideas.md | 3 - Modules/app-hub-module/README.md | 6 +- .../app-hub-module/include/app_hub/app_hub.h | 22 +- .../private/app_hub/app_hub_service.h | 4 + .../app-hub-module/source/app_hub_command.cpp | 294 ++++++++++++++++++ .../app-hub-module/source/app_hub_service.cpp | 68 +++- Modules/app-hub-module/source/module.cpp | 21 +- .../tests/source/app_hub_test.cpp | 22 ++ Modules/lvgl-module/generate-icons.py | 1 + .../lvgl-module/include/lvgl/icons/names.h | 2 +- .../lvgl-module/include/lvgl/icons/shared.h | 1 + Modules/lvgl-module/source/icon_names.c | 1 + Tactility/Source/app/apphub/AppHubApp.cpp | 99 +++++- 15 files changed, 517 insertions(+), 28 deletions(-) create mode 100644 Modules/app-hub-module/source/app_hub_command.cpp diff --git a/Data/system/fonts/MaterialSymbolsRounded.codepoints b/Data/system/fonts/MaterialSymbolsRounded.codepoints index 757c28473..d05798477 100644 --- a/Data/system/fonts/MaterialSymbolsRounded.codepoints +++ b/Data/system/fonts/MaterialSymbolsRounded.codepoints @@ -42,6 +42,7 @@ forum e8af gamepad e30f hard_disk f3da help e8fd +hourglass ebff hub e9f4 image e3f4 info e88e diff --git a/Data/system/fonts/MaterialSymbolsRounded.ttf b/Data/system/fonts/MaterialSymbolsRounded.ttf index 6234dbbb842e827455a7ef64d3baed76e25223b7..78bd51939391543312a7fb4e8914766c22cccede 100644 GIT binary patch delta 1495 zcmX|>e@q)?7{|ZYmX_ab;d)wFyTd(oS7}(tooKsC0~uy zO<$vmQEUvSp@mI;TNP0)Y{)A0m?Goq!jWu8t;1no0qoz|C&|8Q@0EU%K9vqg?NYWR zNp=ag!#1z2-d1V-$U0~ZTaN+O7R$d@lQrFP!}6gyVZJTK#9=Wio)E{yy<(bJEc_yz z5`uy#XiNeA3jZdZ4SbXz+Kw`TkSWb96V zEv-GJLU(5K&1vs+yu++~QIpRNk*$*_G-+~NW3Q^ELL9_a=Yp$LU|0$brh zx-w5grP(|>fjbop?3OE&T$U;zzJ~!L>0SnE<)NgK=0oIB4Hakw#6MxsT8Q6bAV_+L zfnC5|@)h79eT#u=c`>OZ!w3AC$3UIz(Uvqn4cwz5DWD#heU70NboLttsK&F`8Q4zx zGXnwA83w#^Oe=Og3*2`xU?#0)U_WUS0|!W7V4#vT%s>_}x03mU#2Iln))Q}c4l)BA-!GNE1g@HzSRClC?a=Z{{ppEn%15}5F4F(E;MH>T8 z0)N*rP$oN*UEEH&G1)b;4_NA9P}Q+C%0M~kdkj29I>A6I@J|;54Wzdj%4d_HeI%5s z(HkV_xeC-rNKoz+sH2de+$m6RA<+T!Du&t&2}+LwH5?KL8Ps}6&;km`2bQT_6hL!U zt}b~qi&&Rx!To%6XTa&OWOlR*1Zt*WP~4Yl-1r9*$&oEo-|Bq4^N+NqAv zf1n=Kl*1OFC)Z!@&eNI#0lz=j#0%;?x4%5#^O31=yZwGwo}1jQVm0wz^E-f{9tJGdESI5Ky*NOX%ZK~UGS#Sx5pZcm%v`M=Zq?z#7T z^y1sf-owh!ZM7}00!Vcw6RG}v)^~ud6Ue{blju*;?{1)FkhHL;ch}=3o9a4%A{A(u zI?z$xKCD#&%Dc4uNpD|Q;-ATqD!|bs{bXWi3Qyn+t+%VVjjBoe*>gue)XM0Q8G+Zwgrw>DUD%V(LEWx?ERE;3h{ zb;eO6Y4jRhfYBJ7HZ-F+I2t?>$OJCvhxJ|hPW>tUklw5p>s9_A{LlGY{JLMAU8{Yp z4bpeD_NYId4!1ysn#u>lBpoMgd!CEnvrz>}fnY@_u zcHq}i26v0a`7!4`z$I#h1gk_UUr*D$E}vzvg!Fp`car|dP*%8nj-mW8;bQPGX&Hk6 z=?Vr9lCEd)5b0J1l=noEffx8~E#Q>~wdBwYOVB6+1xuZ+z?EAJ){D*py?hyPwVIJl zxq5=ZV$vxFtHr^B$7-mU*Txx8F|S=>K;wShWI$K_YbApkQD0c5tPvfBW$8FD+09TJ zF*(HG4${{d(7wqJ7~BVB=$|G*1L;qUoc}DfB}*|-MM@=+&|n}-C6UmGAiIH~VL_I1 zPD?{`gKQf^6ND_yfP`iVSvo+1a^ME_Ujhm;^%a94@OP8}#hCe$!8*A9+mQoUnv1m$ zC-A#ct$e0T+nQ~s?W%o56)!mIdj?$>-3#5%yU%*Uo<7glo`1dDynp(fz7F4~zKoX8 z_Gs^EW3$`*A^#TtsP52*_49#^fwzMDgBc@kJY$?PrbDHnw$RI=Z%wD!WS%iIR@yoj f-V#0?c`b4!S{Hq3j&IKH`. - `app_hub_search()` lists the catalog apps that are compatible with the device, merged with all installed apps. -- `app_hub_refresh()`, `app_hub_install()` and `app_hub_remove()` queue an operation. Operations run one at a time +- `app_hub_refresh()`, `app_hub_install()`, `app_hub_update_all()` and `app_hub_remove()` queue an operation. Operations run one at a time on a background task, which stops after 5 seconds without operations and starts again for the next one. - Subscribe with `app_hub_event_subscribe()`, then wait with `app_hub_event_await()` and read with `app_hub_event_poll()`. - The events are `REFRESHING`/`REFRESHED`, `INSTALLING`/`INSTALLED` and `REMOVING`/`REMOVED`, with the app id and the result. + The events are `REFRESHING`/`REFRESHED`, `INSTALLING`/`INSTALLED`, `REMOVING`/`REMOVED` and `UPDATING`/`UPDATED`, with the app id and the result. + +The `apphub` shell command uses the same API: `refresh`, `search [query]`, `updates`, `info `, `install `, `update` and `remove `. The catalog is stored in the service's user data directory, so it's available offline after the first refresh. diff --git a/Modules/app-hub-module/include/app_hub/app_hub.h b/Modules/app-hub-module/include/app_hub/app_hub.h index 071628975..193568d8c 100644 --- a/Modules/app-hub-module/include/app_hub/app_hub.h +++ b/Modules/app-hub-module/include/app_hub/app_hub.h @@ -28,6 +28,8 @@ enum AppHubOperationType { APP_HUB_OPERATION_INSTALL, /** Uninstall an app */ APP_HUB_OPERATION_REMOVE, + /** Install the newer catalog version of every installed app that has one */ + APP_HUB_OPERATION_UPDATE_ALL, }; /** An app from the catalog, an installed app, or both */ @@ -89,6 +91,9 @@ error_t app_hub_get_description(const char* id, char* description, size_t descri // region Operations +/** @return true while an operation is queued or running */ +bool app_hub_is_busy(void); + /** * Queue a download of the app catalog. * Only one refresh is queued or running at a time: while one is pending, its id is returned instead. @@ -105,6 +110,17 @@ error_t app_hub_refresh(uint32_t* operation_id); */ error_t app_hub_install(const char* id, uint32_t* operation_id); +/** + * Queue the update of every installed app that has a newer version in the catalog. The apps are installed one at a time, + * with an INSTALLING and INSTALLED event each. A failed update doesn't stop the updates of the other apps. + * While the operation is pending, the apps with an update report it as their pending operation. + * The operation takes the place of an install or remove: only one of them is queued or running at a time. + * @param[out] operation_id the id of the queued operation (optional) + * @retval ERROR_NONE the operation was queued, or it was already pending and its id is returned + * @retval ERROR_RESOURCE_BUSY an install or remove is pending + */ +error_t app_hub_update_all(uint32_t* operation_id); + /** * Queue the removal of an installed app. * Only one install or remove is queued or running at a time. A refresh can be pending next to it. @@ -131,12 +147,16 @@ enum AppHubEventType { APP_HUB_EVENT_REMOVING, /** The removal of AppHubEvent.app_id finished, see AppHubEvent.result */ APP_HUB_EVENT_REMOVED, + /** Updating all apps started. INSTALLING and INSTALLED events with the same operation id follow for each app. */ + APP_HUB_EVENT_UPDATING, + /** Updating all apps finished. AppHubEvent.result is the error of the last failed app, ERROR_NONE when all succeeded. */ + APP_HUB_EVENT_UPDATED, }; struct AppHubEvent { enum AppHubEventType type; uint32_t operation_id; - /** Empty for the refresh events */ + /** Empty for the refresh, UPDATING and UPDATED events */ PackageId app_id; /** The outcome of the operation for the REFRESHED, INSTALLED and REMOVED events, ERROR_NONE otherwise */ error_t result; diff --git a/Modules/app-hub-module/private/app_hub/app_hub_service.h b/Modules/app-hub-module/private/app_hub/app_hub_service.h index 3670c4d11..2d7c2f4b1 100644 --- a/Modules/app-hub-module/private/app_hub/app_hub_service.h +++ b/Modules/app-hub-module/private/app_hub/app_hub_service.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include #include /** The service id, which also determines where the catalog is stored */ @@ -8,5 +9,8 @@ extern const ServiceManifest app_hub_service_manifest; +/** The "apphub" command-line app */ +extern "C" const AppManifest app_hub_command_manifest; + /** @return true while the background task that runs the operations exists */ bool app_hub_is_worker_running(); diff --git a/Modules/app-hub-module/source/app_hub_command.cpp b/Modules/app-hub-module/source/app_hub_command.cpp new file mode 100644 index 000000000..6b516a87a --- /dev/null +++ b/Modules/app-hub-module/source/app_hub_command.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +constexpr TickType_t OPERATION_TIMEOUT = pdMS_TO_TICKS(180000); +constexpr size_t DESCRIPTION_MAX_SIZE = 2048; + +static void print_usage() { + printf( + "usage: apphub \n" + " refresh\n" + " search [query]\n" + " updates\n" + " info \n" + " install \n" + " update\n" + " remove \n" + ); +} + +static int report_error(const char* action, error_t error) { + if (error == ERROR_RESOURCE_BUSY) { + printf("apphub: %s failed: another app is being installed or removed\n", action); + } else { + printf("apphub: %s failed: %s\n", action, error_to_string(error)); + } + return 1; +} + +/** + * Queues an operation and waits for its result. + * The subscription is made before queueing, so the operation can't finish unnoticed. + * @param[in] finished_type the event that ends the operation and carries its result + * @param[in] on_event called for the other events of the operation (optional) + */ +static error_t run_and_wait( + const char* id, + error_t (*operation)(const char* id, uint32_t* operation_id), + AppHubEventType finished_type, + void (*on_event)(const AppHubEvent& event) = nullptr +) { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppHubEventSubscription subscription {}; + error_t error = app_hub_event_subscribe(&subscription, &event_group); + if (error != ERROR_NONE) { + task_event_group_destruct(&event_group); + return error; + } + + uint32_t operation_id = 0; + error = operation(id, &operation_id); + if (error == ERROR_NONE) { + error = ERROR_TIMEOUT; + const TickType_t start = get_ticks(); + TickType_t elapsed = 0; + bool finished = false; + while (!finished && elapsed < OPERATION_TIMEOUT) { + if (app_hub_event_await(&subscription, OPERATION_TIMEOUT - elapsed) == ERROR_NONE) { + AppHubEvent event {}; + while (!finished && app_hub_event_poll(&subscription, &event) == ERROR_NONE) { + if (event.operation_id != operation_id) { + continue; + } + if (event.type == finished_type) { + error = event.result; + finished = true; + } else if (on_event != nullptr) { + on_event(event); + } + } + } + elapsed = get_ticks() - start; + } + } + + app_hub_event_unsubscribe(&subscription); + task_event_group_destruct(&event_group); + return error; +} + +static error_t refresh(const char* /*id*/, uint32_t* operation_id) { + return app_hub_refresh(operation_id); +} + +/** Collects the apps first, as printing can block while app_hub_search() holds the service's lock */ +static std::vector search(const char* query) { + std::vector apps; + app_hub_search(query, [](const AppHubApp* app, void* context) { + static_cast*>(context)->push_back(*app); + return true; + }, &apps); + return apps; +} + +static void print_app(const AppHubApp& app) { + const char marker = app.update_available ? '^' : (app.installed ? '*' : ' '); + const char* version = app.in_catalog ? app.version_name : app.installed_version_name; + printf("%c %-32s %-24s %s\n", marker, app.id, app.name, version); +} + +static int command_refresh() { + printf("Downloading the catalog...\n"); + const error_t error = run_and_wait(nullptr, refresh, APP_HUB_EVENT_REFRESHED); + if (error != ERROR_NONE) { + return report_error("refresh", error); + } + printf("Catalog updated\n"); + return 0; +} + +static int command_search(int argc, char* argv[], bool updates_only) { + if (argc > 3 || (updates_only && argc > 2)) { + print_usage(); + return 1; + } + + const auto apps = search(argc == 3 ? argv[2] : nullptr); + size_t count = 0; + for (const auto& app : apps) { + if (!updates_only || app.update_available) { + print_app(app); + count++; + } + } + if (count == 0) { + printf(updates_only ? "No updates available\n" : "No apps found. Run 'apphub refresh' to download the catalog.\n"); + } else if (!updates_only) { + printf("(* = installed, ^ = update available)\n"); + } + return 0; +} + +static int command_info(int argc, char* argv[]) { + if (argc != 3) { + printf("usage: apphub info \n"); + return 1; + } + + AppHubApp app {}; + if (app_hub_get_app(argv[2], &app) != ERROR_NONE) { + printf("apphub: unknown app: %s\n", argv[2]); + return 1; + } + + printf("id: %s\n", app.id); + printf("name: %s\n", app.name); + if (app.in_catalog) { + printf("version: %s\n", app.version_name); + } + if (app.installed) { + printf("installed: %s%s\n", app.installed_version_name, app.update_available ? " (update available)" : ""); + } else { + printf("installed: no\n"); + } + + auto description = std::make_unique(DESCRIPTION_MAX_SIZE); + if (app_hub_get_description(app.id, description.get(), DESCRIPTION_MAX_SIZE) == ERROR_NONE && description[0] != '\0') { + printf("\n%s\n", description.get()); + } + return 0; +} + +static int command_install(int argc, char* argv[]) { + if (argc != 3) { + printf("usage: apphub install \n"); + return 1; + } + + printf("Installing %s...\n", argv[2]); + const error_t error = run_and_wait(argv[2], app_hub_install, APP_HUB_EVENT_INSTALLED); + if (error == ERROR_NOT_FOUND) { + printf("apphub: %s is not in the catalog\n", argv[2]); + return 1; + } else if (error != ERROR_NONE) { + return report_error("install", error); + } + printf("Installed %s\n", argv[2]); + return 0; +} + +static error_t update_all(const char* /*id*/, uint32_t* operation_id) { + return app_hub_update_all(operation_id); +} + +static void print_update_progress(const AppHubEvent& event) { + if (event.type == APP_HUB_EVENT_INSTALLING) { + printf("Updating %s...\n", event.app_id); + } else if (event.type == APP_HUB_EVENT_INSTALLED) { + if (event.result == ERROR_NONE) { + printf("Updated %s\n", event.app_id); + } else { + printf("apphub: update of %s failed: %s\n", event.app_id, error_to_string(event.result)); + } + } +} + +static int command_update(int argc) { + if (argc != 2) { + printf("usage: apphub update\n"); + return 1; + } + + // Counted first, as the update reports nothing when there are no updates + size_t count = 0; + for (const auto& app : search(nullptr)) { + if (app.update_available) { + count++; + } + } + if (count == 0) { + printf("No updates available\n"); + return 0; + } + + const error_t error = run_and_wait(nullptr, update_all, APP_HUB_EVENT_UPDATED, print_update_progress); + if (error == ERROR_RESOURCE_BUSY || error == ERROR_TIMEOUT) { + return report_error("update", error); + } else if (error != ERROR_NONE) { + printf("apphub: some apps failed to update\n"); + return 1; + } + printf("All apps are up to date\n"); + return 0; +} + +static int command_remove(int argc, char* argv[]) { + if (argc != 3) { + printf("usage: apphub remove \n"); + return 1; + } + + printf("Removing %s...\n", argv[2]); + const error_t error = run_and_wait(argv[2], app_hub_remove, APP_HUB_EVENT_REMOVED); + if (error == ERROR_NOT_FOUND) { + printf("apphub: %s is not installed\n", argv[2]); + return 1; + } else if (error != ERROR_NONE) { + return report_error("remove", error); + } + printf("Removed %s\n", argv[2]); + return 0; +} + +static int32_t app_hub_command_main(int argc, char* argv[]) { + if (argc < 2 || strcmp(argv[1], "help") == 0) { + print_usage(); + return 0; + } + + const char* command = argv[1]; + if (strcmp(command, "refresh") == 0) { + return command_refresh(); + } else if (strcmp(command, "search") == 0) { + return command_search(argc, argv, false); + } else if (strcmp(command, "updates") == 0) { + return command_search(argc, argv, true); + } else if (strcmp(command, "info") == 0) { + return command_info(argc, argv); + } else if (strcmp(command, "install") == 0) { + return command_install(argc, argv); + } else if (strcmp(command, "update") == 0) { + return command_update(argc); + } else if (strcmp(command, "remove") == 0) { + return command_remove(argc, argv); + } + + printf("apphub: unknown command: %s\n", command); + print_usage(); + return 1; +} + +extern "C" { + +const AppManifest app_hub_command_manifest = { + .id = "apphub", + .name = "apphub", + .category = APP_CATEGORY_SYSTEM, + .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast(app_hub_command_main) }, + .flags = APP_MANIFEST_FLAG_HEADLESS, + .stack = { .depth = 5120, .desired_memory_capability = 0 }, +}; + +} diff --git a/Modules/app-hub-module/source/app_hub_service.cpp b/Modules/app-hub-module/source/app_hub_service.cpp index 0056b7100..29ac3397e 100644 --- a/Modules/app-hub-module/source/app_hub_service.cpp +++ b/Modules/app-hub-module/source/app_hub_service.cpp @@ -86,6 +86,7 @@ AppHubEventType getStartedEventType(AppHubOperationType operation) { case APP_HUB_OPERATION_REFRESH: return APP_HUB_EVENT_REFRESHING; case APP_HUB_OPERATION_INSTALL: return APP_HUB_EVENT_INSTALLING; case APP_HUB_OPERATION_REMOVE: return APP_HUB_EVENT_REMOVING; + case APP_HUB_OPERATION_UPDATE_ALL: return APP_HUB_EVENT_UPDATING; } return APP_HUB_EVENT_REFRESHING; } @@ -95,16 +96,20 @@ AppHubEventType getFinishedEventType(AppHubOperationType operation) { case APP_HUB_OPERATION_REFRESH: return APP_HUB_EVENT_REFRESHED; case APP_HUB_OPERATION_INSTALL: return APP_HUB_EVENT_INSTALLED; case APP_HUB_OPERATION_REMOVE: return APP_HUB_EVENT_REMOVED; + case APP_HUB_OPERATION_UPDATE_ALL: return APP_HUB_EVENT_UPDATED; } return APP_HUB_EVENT_REFRESHED; } -/** Requires the global lock */ -void emitEvent(AppHubEventType type, const Operation& operation, error_t result) { +/** + * Requires the global lock + * @param[in] appId the app the event is about, which an update of all apps reports per app + */ +void emitEvent(AppHubEventType type, const Operation& operation, const char* appId, error_t result) { AppHubEvent event {}; event.type = type; event.operation_id = operation.id; - strcpy(event.app_id, operation.app_id); + strcpy(event.app_id, appId); event.result = result; for (auto* sub = subscriptions; sub != nullptr; sub = sub->internal.next) { if (sub->internal.count >= APP_HUB_EVENT_QUEUE_CAPACITY) { @@ -235,8 +240,13 @@ AppList collectApps(const std::vector& installedPackages) { app->update_available = app->in_catalog && app->installed_version_code < app->version_code; } + const Operation* pendingAppOperation = findPendingOperationOfKind(false); + const bool updatingAll = pendingAppOperation != nullptr && pendingAppOperation->type == APP_HUB_OPERATION_UPDATE_ALL; for (auto& app : apps) { const Operation* pending = findPendingOperation(app.id); + if (pending == nullptr && updatingAll && app.update_available) { + pending = pendingAppOperation; + } app.operation_pending = pending != nullptr; if (pending != nullptr) { app.pending_operation = pending->type; @@ -374,6 +384,41 @@ error_t executeInstall(const char* id) { return error; } +error_t executeUpdateAll(const Operation& operation) { + const auto installed = getInstalledPackages(); + std::vector ids; + lock(); + for (const auto& app : collectApps(installed)) { + if (app.update_available) { + ids.emplace_back(app.id); + } + } + unlock(); + + error_t lastError = ERROR_NONE; + for (const auto& id : ids) { + lock(); + const bool stop = stopping; + if (!stop) { + emitEvent(APP_HUB_EVENT_INSTALLING, operation, id.c_str(), ERROR_NONE); + } + unlock(); + if (stop) { + return ERROR_INVALID_STATE; + } + + const error_t error = executeInstall(id.c_str()); + if (error != ERROR_NONE) { + lastError = error; + } + + lock(); + emitEvent(APP_HUB_EVENT_INSTALLED, operation, id.c_str(), error); + unlock(); + } + return lastError; +} + error_t execute(const Operation& operation) { switch (operation.type) { case APP_HUB_OPERATION_REFRESH: @@ -382,6 +427,8 @@ error_t execute(const Operation& operation) { return executeInstall(operation.app_id); case APP_HUB_OPERATION_REMOVE: return app_uninstall(operation.app_id); + case APP_HUB_OPERATION_UPDATE_ALL: + return executeUpdateAll(operation); } return ERROR_INVALID_ARGUMENT; } @@ -417,14 +464,14 @@ int32_t workerMain(void* /*context*/) { queue.pop_front(); operationRunning = true; const Operation operation = runningOperation; - emitEvent(getStartedEventType(operation.type), operation, ERROR_NONE); + emitEvent(getStartedEventType(operation.type), operation, operation.app_id, ERROR_NONE); unlock(); const error_t result = execute(operation); lock(); operationRunning = false; - emitEvent(getFinishedEventType(operation.type), operation, result); + emitEvent(getFinishedEventType(operation.type), operation, operation.app_id, result); unlock(); } } @@ -611,6 +658,13 @@ error_t app_hub_get_description(const char* id, char* description, size_t descri // region Operations +bool app_hub_is_busy() { + lock(); + const bool busy = operationRunning || !queue.empty(); + unlock(); + return busy; +} + error_t app_hub_refresh(uint32_t* operation_id) { return enqueue(APP_HUB_OPERATION_REFRESH, nullptr, operation_id); } @@ -619,6 +673,10 @@ error_t app_hub_install(const char* id, uint32_t* operation_id) { return enqueue(APP_HUB_OPERATION_INSTALL, id, operation_id); } +error_t app_hub_update_all(uint32_t* operation_id) { + return enqueue(APP_HUB_OPERATION_UPDATE_ALL, nullptr, operation_id); +} + error_t app_hub_remove(const char* id, uint32_t* operation_id) { return enqueue(APP_HUB_OPERATION_REMOVE, id, operation_id); } diff --git a/Modules/app-hub-module/source/module.cpp b/Modules/app-hub-module/source/module.cpp index 0e26b81a1..487cca502 100644 --- a/Modules/app-hub-module/source/module.cpp +++ b/Modules/app-hub-module/source/module.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -11,23 +12,37 @@ extern "C" { static error_t start() { - return service_manager_add(&app_hub_service_manifest, /*auto_start=*/true); + error_t error = app_manager_add(&app_hub_command_manifest); + if (error != ERROR_NONE) { + return error; + } + error = service_manager_add(&app_hub_service_manifest, /*auto_start=*/true); + if (error != ERROR_NONE) { + app_manager_remove(app_hub_command_manifest.id); + } + return error; } static error_t stop() { - const error_t error = service_manager_stop(app_hub_service_manifest.id); + error_t error = service_manager_stop(app_hub_service_manifest.id); if (error != ERROR_NONE && error != ERROR_NOT_FOUND) { return error; } - return service_manager_remove(app_hub_service_manifest.id); + error = service_manager_remove(app_hub_service_manifest.id); + if (error != ERROR_NONE) { + return error; + } + return app_manager_remove(app_hub_command_manifest.id); } static const ModuleSymbol app_hub_module_symbols[] = { DEFINE_MODULE_SYMBOL(app_hub_search), DEFINE_MODULE_SYMBOL(app_hub_get_app), DEFINE_MODULE_SYMBOL(app_hub_get_description), + DEFINE_MODULE_SYMBOL(app_hub_is_busy), DEFINE_MODULE_SYMBOL(app_hub_refresh), DEFINE_MODULE_SYMBOL(app_hub_install), + DEFINE_MODULE_SYMBOL(app_hub_update_all), DEFINE_MODULE_SYMBOL(app_hub_remove), DEFINE_MODULE_SYMBOL(app_hub_event_subscribe), DEFINE_MODULE_SYMBOL(app_hub_event_unsubscribe), diff --git a/Modules/app-hub-module/tests/source/app_hub_test.cpp b/Modules/app-hub-module/tests/source/app_hub_test.cpp index a9c4cb702..258ed4967 100644 --- a/Modules/app-hub-module/tests/source/app_hub_test.cpp +++ b/Modules/app-hub-module/tests/source/app_hub_test.cpp @@ -249,6 +249,28 @@ TEST_CASE("app_hub drops newer events while a subscriber's queue is full") { task_event_group_destruct(&group); } +TEST_CASE("app_hub_update_all finishes without installs when nothing has an update") { + ensureFixture(); + TaskEventGroup group {}; + task_event_group_construct(&group); + AppHubEventSubscription sub {}; + REQUIRE_EQ(app_hub_event_subscribe(&sub, &group), ERROR_NONE); + + uint32_t id = 0; + REQUIRE_EQ(app_hub_update_all(&id), ERROR_NONE); + AppHubEvent event = waitForEvent(sub); + CHECK_EQ(event.type, APP_HUB_EVENT_UPDATING); + CHECK_EQ(event.operation_id, id); + CHECK_EQ(std::string(event.app_id), ""); + event = waitForEvent(sub); + CHECK_EQ(event.type, APP_HUB_EVENT_UPDATED); + CHECK_EQ(event.operation_id, id); + CHECK_EQ(event.result, ERROR_NONE); + + CHECK_EQ(app_hub_event_unsubscribe(&sub), ERROR_NONE); + task_event_group_destruct(&group); +} + TEST_CASE("app_hub rejects invalid ids") { ensureFixture(); CHECK_EQ(app_hub_install("", nullptr), ERROR_INVALID_ARGUMENT); diff --git a/Modules/lvgl-module/generate-icons.py b/Modules/lvgl-module/generate-icons.py index a4ff4b437..ad87cd2b0 100755 --- a/Modules/lvgl-module/generate-icons.py +++ b/Modules/lvgl-module/generate-icons.py @@ -159,6 +159,7 @@ shared_symbol_code_point_names = [ "language", # Globe "lists", # Chat app toolbar "list_alt", # Settings apps: opens a selection dialog + "hourglass", # App Hub: an operation for the app is pending "mail", "menu", "memory", # System Info diff --git a/Modules/lvgl-module/include/lvgl/icons/names.h b/Modules/lvgl-module/include/lvgl/icons/names.h index 1cb0d28df..ef5ce124e 100644 --- a/Modules/lvgl-module/include/lvgl/icons/names.h +++ b/Modules/lvgl-module/include/lvgl/icons/names.h @@ -9,7 +9,7 @@ extern "C" { #endif /** Changes whenever the icon TTF subset, its codepoints file or the icon name lists change */ -#define LVGL_ICON_FONT_VERSION 0xE8678DFBu +#define LVGL_ICON_FONT_VERSION 0x3F505982u // Icon names per icon font, resolved through the .codepoints file next to the TTF extern const char* const lvgl_icon_shared_names[]; diff --git a/Modules/lvgl-module/include/lvgl/icons/shared.h b/Modules/lvgl-module/include/lvgl/icons/shared.h index 1885ed0ac..5f16a9cae 100644 --- a/Modules/lvgl-module/include/lvgl/icons/shared.h +++ b/Modules/lvgl-module/include/lvgl/icons/shared.h @@ -32,6 +32,7 @@ #define LVGL_ICON_SHARED_LANGUAGE "\xEE\xA8\x87" #define LVGL_ICON_SHARED_LISTS "\xEE\xA6\xB9" #define LVGL_ICON_SHARED_LIST_ALT "\xEE\x83\xAE" +#define LVGL_ICON_SHARED_HOURGLASS "\xEE\xAF\xBF" #define LVGL_ICON_SHARED_MAIL "\xEE\x85\x99" #define LVGL_ICON_SHARED_MENU "\xEE\x97\x92" #define LVGL_ICON_SHARED_MEMORY "\xEE\x8C\xA2" diff --git a/Modules/lvgl-module/source/icon_names.c b/Modules/lvgl-module/source/icon_names.c index c7a572138..76d76f6d7 100644 --- a/Modules/lvgl-module/source/icon_names.c +++ b/Modules/lvgl-module/source/icon_names.c @@ -34,6 +34,7 @@ const char* const lvgl_icon_shared_names[] = { "language", "lists", "list_alt", + "hourglass", "mail", "menu", "memory", diff --git a/Tactility/Source/app/apphub/AppHubApp.cpp b/Tactility/Source/app/apphub/AppHubApp.cpp index 33ce11876..2991aef89 100644 --- a/Tactility/Source/app/apphub/AppHubApp.cpp +++ b/Tactility/Source/app/apphub/AppHubApp.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -14,7 +15,9 @@ #include #include +#include #include +#include #include #include #include @@ -50,6 +53,8 @@ struct Context { lv_obj_t* contentWrapper = nullptr; lv_obj_t* refreshButton = nullptr; + lv_obj_t* updateAllButton = nullptr; + lv_obj_t* updateAllSpinner = nullptr; lv_obj_t* filterChips = nullptr; // Survives across a bury/resurface cycle Filter filter = Filter::All; @@ -74,6 +79,10 @@ struct Context { AppHubEventSubscription appHubSub {}; /** The id of the queued catalog refresh, 0 when none is pending */ std::atomic refreshOperationId {0}; + /** The id of the running update of all apps, 0 when none is running */ + uint32_t updateAllOperationId = 0; + /** The ids of the apps that failed to update during updateAllOperationId */ + std::vector failedUpdates; }; @@ -103,6 +112,30 @@ void updateFilterChips(Context* ctx) { } } +/** + * Shows the spinner while App Hub is busy, and otherwise the update all button when there are updates. + * Requires the LVGL lock. + */ +void updateToolbarActions(Context* ctx, bool hasUpdates) { + if (ctx->updateAllSpinner == nullptr) { + return; + } + const bool busy = app_hub_is_busy(); + lv_obj_set_hidden(ctx->updateAllSpinner, !busy); + lv_obj_set_hidden(ctx->updateAllButton, busy || !hasUpdates); +} + +void onUpdateAllPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + const error_t error = app_hub_update_all(nullptr); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to queue the update of all apps: %s", error_to_string(error)); + return; + } + // Shows the spinner + showApps(ctx); +} + void updateUpdatesChipText(Context* ctx, size_t updateCount) { auto* chip = lv_obj_get_child(ctx->filterChips, static_cast(Filter::Updates)); const auto text = std::format("{} {}", updateCount, updateCount == 1 ? "update" : "updates"); @@ -189,6 +222,7 @@ void showRefreshFailedError(Context* ctx, const char* message) { lv_label_set_text(label, message); lv_obj_set_hidden(ctx->refreshButton, false); + updateToolbarActions(ctx, false); } void showNoInternet(Context* ctx) { @@ -236,6 +270,7 @@ void showApps(Context* ctx) { }, &apps); const bool hasApps = !apps.empty(); updateUpdatesChipText(ctx, std::ranges::count_if(apps, [](const AppHubApp& app) { return app.update_available; })); + updateToolbarActions(ctx, std::ranges::any_of(apps, [](const AppHubApp& app) { return app.update_available; })); std::erase_if(apps, [ctx](const AppHubApp& app) { return !matchesFilter(app, ctx->filter); }); @@ -256,8 +291,11 @@ void showApps(Context* ctx) { lv_obj_t* focus_target = nullptr; for (int i = 0; i < apps.size(); i++) { const auto& app = apps[i]; - const char* icon = app.installed ? LV_SYMBOL_OK : nullptr; + const char* icon = app.operation_pending ? LVGL_ICON_SHARED_HOURGLASS : (app.installed ? LV_SYMBOL_OK : nullptr); auto* entry_button = lvgl_list_add_button(list, icon, app.name); + if (app.operation_pending) { + lv_obj_set_style_text_font(lv_obj_get_child(entry_button, 0), lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT); + } auto int_as_voidptr = reinterpret_cast(i); lv_obj_set_user_data(entry_button, int_as_voidptr); lv_obj_add_event_cb(entry_button, onAppPressed, LV_EVENT_SHORT_CLICKED, ctx); @@ -331,7 +369,17 @@ void refresh(Context* ctx) { lvgl_unlock(); } -void onOperationFinished(Context* ctx, const AppHubEvent& event) { +void onOperationEvent(Context* ctx, const AppHubEvent& event) { + if (event.type == APP_HUB_EVENT_REFRESHING) { + // The list doesn't change yet, only the toolbar shows that App Hub is busy + lvgl_lock(); + if (ctx->updateAllButton != nullptr) { + updateToolbarActions(ctx, !lv_obj_is_hidden(ctx->updateAllButton)); + } + lvgl_unlock(); + return; + } + const bool isRefresh = event.operation_id == ctx->refreshOperationId; if (isRefresh) { ctx->refreshOperationId = 0; @@ -341,13 +389,10 @@ void onOperationFinished(Context* ctx, const AppHubEvent& event) { } } - if (ctx->contentWrapper == nullptr) { - // Buried (e.g. AppHubDetailsApp is open): createWidgets() rebuilds the list on resurface - return; - } - lvgl_lock(); - if (isRefresh && event.result != ERROR_NONE) { + if (ctx->contentWrapper == nullptr) { + // Buried (e.g. AppHubDetailsApp or a dialog is open): createWidgets() rebuilds the list on resurface + } else if (isRefresh && event.result != ERROR_NONE) { showRefreshFailedError(ctx, "Cannot reach server"); } else { showApps(ctx); @@ -366,6 +411,10 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); ctx->refreshButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_REFRESH, onRefreshPressed, ctx); lv_obj_set_hidden(ctx->refreshButton, true); + ctx->updateAllButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_DOWNLOAD, onUpdateAllPressed, ctx); + lv_obj_set_hidden(ctx->updateAllButton, true); + ctx->updateAllSpinner = lvgl_toolbar_add_spinner_action(toolbar); + lv_obj_set_hidden(ctx->updateAllSpinner, true); ctx->filterChips = createFilterChips(parent, ctx); updateFilterChips(ctx); @@ -391,9 +440,36 @@ void destroyWidgets(void* userData) { ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper); ctx->contentWrapper = nullptr; ctx->refreshButton = nullptr; + ctx->updateAllButton = nullptr; + ctx->updateAllSpinner = nullptr; ctx->filterChips = nullptr; } +/** Tracks the update of all apps, and shows which apps failed when it's done */ +void onUpdateAllEvent(Context* ctx, const AppHubEvent& event) { + if (event.type == APP_HUB_EVENT_UPDATING) { + ctx->updateAllOperationId = event.operation_id; + ctx->failedUpdates.clear(); + } else if (event.type == APP_HUB_EVENT_INSTALLED && event.operation_id == ctx->updateAllOperationId && event.result != ERROR_NONE) { + ctx->failedUpdates.emplace_back(event.app_id); + } else if (event.type == APP_HUB_EVENT_UPDATED && event.operation_id == ctx->updateAllOperationId) { + ctx->updateAllOperationId = 0; + if (!ctx->failedUpdates.empty()) { + std::string names; + for (const auto& id : ctx->failedUpdates) { + AppHubApp app {}; + if (!names.empty()) { + names += ", "; + } + names += (app_hub_get_app(id.c_str(), &app) == ERROR_NONE) ? app.name : id.c_str(); + } + const auto count = ctx->failedUpdates.size(); + alertdialog::start(ctx->appInstanceId, "Update failed", std::format("{} {} failed to update: {}", count, count == 1 ? "app" : "apps", names)); + ctx->failedUpdates.clear(); + } + } +} + int32_t appMain(int argc, char* argv[]) { uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx; @@ -429,11 +505,8 @@ int32_t appMain(int argc, char* argv[]) { AppHubEvent app_hub_event {}; while (app_hub_event_poll(&ctx.appHubSub, &app_hub_event) == ERROR_NONE) { - if (app_hub_event.type == APP_HUB_EVENT_REFRESHED || - app_hub_event.type == APP_HUB_EVENT_INSTALLED || - app_hub_event.type == APP_HUB_EVENT_REMOVED) { - onOperationFinished(&ctx, app_hub_event); - } + onUpdateAllEvent(&ctx, app_hub_event); + onOperationEvent(&ctx, app_hub_event); } if (!shouldClose && ctx.refreshRequested.exchange(false)) {