App Hub: update-all, commandline app, better progress indication

This commit is contained in:
Ken Van Hoeylandt 2026-10-15 19:28:36 +02:00
parent 87215baf55
commit 582797aa4c
15 changed files with 517 additions and 28 deletions

View File

@ -42,6 +42,7 @@ forum e8af
gamepad e30f
hard_disk f3da
help e8fd
hourglass ebff
hub e9f4
image e3f4
info e88e

View File

@ -7,9 +7,6 @@
## Higher Priority
- Adding GPS: Crash when clicking "+"
- Update FreeRTOS-Kernel to 11.3.1 to fix hanging tests, then re-release apps.
- App Hub needs a commandline app
- BlueTooth as radio backend for MeshCore/Meshtastic?
- ESP-NOW as radio backend. Use it for MeshCore/Meshtastic?
- Find out why the first build of an app with a clean build cache folder always fails

View File

@ -4,10 +4,12 @@ App Hub service (`tactility.apphub`): finds, installs and removes apps from the
The C API is in `<app_hub/app_hub.h>`.
- `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 <id>`, `install <id>`, `update` and `remove <id>`.
The catalog is stored in the service's user data directory, so it's available offline after the first refresh.

View File

@ -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;

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <service/manifest.h>
/** 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();

View File

@ -0,0 +1,294 @@
// SPDX-License-Identifier: Apache-2.0
#include <app_hub/app_hub.h>
#include <app_hub/app_hub_service.h>
#include <app/manifest.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/time.h>
#include <cstdio>
#include <cstring>
#include <memory>
#include <vector>
constexpr TickType_t OPERATION_TIMEOUT = pdMS_TO_TICKS(180000);
constexpr size_t DESCRIPTION_MAX_SIZE = 2048;
static void print_usage() {
printf(
"usage: apphub <command>\n"
" refresh\n"
" search [query]\n"
" updates\n"
" info <id>\n"
" install <id>\n"
" update\n"
" remove <id>\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<AppHubApp> search(const char* query) {
std::vector<AppHubApp> apps;
app_hub_search(query, [](const AppHubApp* app, void* context) {
static_cast<std::vector<AppHubApp>*>(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 <id>\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<char[]>(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 <id>\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 <id>\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<void*>(app_hub_command_main) },
.flags = APP_MANIFEST_FLAG_HEADLESS,
.stack = { .depth = 5120, .desired_memory_capability = 0 },
};
}

View File

@ -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<InstalledPackage>& 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<std::string> 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);
}

View File

@ -3,6 +3,7 @@
#include <app_hub/app_hub_service.h>
#include <app_hub/module.h>
#include <app/manager.h>
#include <service/manager.h>
#include <tactility/error.h>
@ -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),

View File

@ -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);

View File

@ -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

View File

@ -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[];

View File

@ -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"

View File

@ -34,6 +34,7 @@ const char* const lvgl_icon_shared_names[] = {
"language",
"lists",
"list_alt",
"hourglass",
"mail",
"menu",
"memory",

View File

@ -1,4 +1,5 @@
#include <Tactility/Mutex.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/app/apphubdetails/AppHubDetailsApp.h>
#include <app/event.h>
@ -14,7 +15,9 @@
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <lvgl/fonts.h>
#include <lvgl/grid_navigation.h>
#include <lvgl/icons/shared.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/chip.h>
@ -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<uint32_t> 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<std::string> 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<Context*>(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<int32_t>(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<void*>(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)) {