mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
App Hub: update-all, commandline app, better progress indication
This commit is contained in:
parent
87215baf55
commit
582797aa4c
@ -42,6 +42,7 @@ forum e8af
|
|||||||
gamepad e30f
|
gamepad e30f
|
||||||
hard_disk f3da
|
hard_disk f3da
|
||||||
help e8fd
|
help e8fd
|
||||||
|
hourglass ebff
|
||||||
hub e9f4
|
hub e9f4
|
||||||
image e3f4
|
image e3f4
|
||||||
info e88e
|
info e88e
|
||||||
|
|||||||
Binary file not shown.
@ -7,9 +7,6 @@
|
|||||||
|
|
||||||
## Higher Priority
|
## 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?
|
- BlueTooth as radio backend for MeshCore/Meshtastic?
|
||||||
- ESP-NOW as radio backend. Use it 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
|
- Find out why the first build of an app with a clean build cache folder always fails
|
||||||
|
|||||||
@ -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>`.
|
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_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.
|
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()`.
|
- 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.
|
The catalog is stored in the service's user data directory, so it's available offline after the first refresh.
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,8 @@ enum AppHubOperationType {
|
|||||||
APP_HUB_OPERATION_INSTALL,
|
APP_HUB_OPERATION_INSTALL,
|
||||||
/** Uninstall an app */
|
/** Uninstall an app */
|
||||||
APP_HUB_OPERATION_REMOVE,
|
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 */
|
/** 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
|
// region Operations
|
||||||
|
|
||||||
|
/** @return true while an operation is queued or running */
|
||||||
|
bool app_hub_is_busy(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue a download of the app catalog.
|
* 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.
|
* 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);
|
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.
|
* 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.
|
* 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,
|
APP_HUB_EVENT_REMOVING,
|
||||||
/** The removal of AppHubEvent.app_id finished, see AppHubEvent.result */
|
/** The removal of AppHubEvent.app_id finished, see AppHubEvent.result */
|
||||||
APP_HUB_EVENT_REMOVED,
|
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 {
|
struct AppHubEvent {
|
||||||
enum AppHubEventType type;
|
enum AppHubEventType type;
|
||||||
uint32_t operation_id;
|
uint32_t operation_id;
|
||||||
/** Empty for the refresh events */
|
/** Empty for the refresh, UPDATING and UPDATED events */
|
||||||
PackageId app_id;
|
PackageId app_id;
|
||||||
/** The outcome of the operation for the REFRESHED, INSTALLED and REMOVED events, ERROR_NONE otherwise */
|
/** The outcome of the operation for the REFRESHED, INSTALLED and REMOVED events, ERROR_NONE otherwise */
|
||||||
error_t result;
|
error_t result;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <app/manifest.h>
|
||||||
#include <service/manifest.h>
|
#include <service/manifest.h>
|
||||||
|
|
||||||
/** The service id, which also determines where the catalog is stored */
|
/** The service id, which also determines where the catalog is stored */
|
||||||
@ -8,5 +9,8 @@
|
|||||||
|
|
||||||
extern const ServiceManifest app_hub_service_manifest;
|
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 */
|
/** @return true while the background task that runs the operations exists */
|
||||||
bool app_hub_is_worker_running();
|
bool app_hub_is_worker_running();
|
||||||
|
|||||||
294
Modules/app-hub-module/source/app_hub_command.cpp
Normal file
294
Modules/app-hub-module/source/app_hub_command.cpp
Normal 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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -86,6 +86,7 @@ AppHubEventType getStartedEventType(AppHubOperationType operation) {
|
|||||||
case APP_HUB_OPERATION_REFRESH: return APP_HUB_EVENT_REFRESHING;
|
case APP_HUB_OPERATION_REFRESH: return APP_HUB_EVENT_REFRESHING;
|
||||||
case APP_HUB_OPERATION_INSTALL: return APP_HUB_EVENT_INSTALLING;
|
case APP_HUB_OPERATION_INSTALL: return APP_HUB_EVENT_INSTALLING;
|
||||||
case APP_HUB_OPERATION_REMOVE: return APP_HUB_EVENT_REMOVING;
|
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;
|
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_REFRESH: return APP_HUB_EVENT_REFRESHED;
|
||||||
case APP_HUB_OPERATION_INSTALL: return APP_HUB_EVENT_INSTALLED;
|
case APP_HUB_OPERATION_INSTALL: return APP_HUB_EVENT_INSTALLED;
|
||||||
case APP_HUB_OPERATION_REMOVE: return APP_HUB_EVENT_REMOVED;
|
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;
|
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 {};
|
AppHubEvent event {};
|
||||||
event.type = type;
|
event.type = type;
|
||||||
event.operation_id = operation.id;
|
event.operation_id = operation.id;
|
||||||
strcpy(event.app_id, operation.app_id);
|
strcpy(event.app_id, appId);
|
||||||
event.result = result;
|
event.result = result;
|
||||||
for (auto* sub = subscriptions; sub != nullptr; sub = sub->internal.next) {
|
for (auto* sub = subscriptions; sub != nullptr; sub = sub->internal.next) {
|
||||||
if (sub->internal.count >= APP_HUB_EVENT_QUEUE_CAPACITY) {
|
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;
|
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) {
|
for (auto& app : apps) {
|
||||||
const Operation* pending = findPendingOperation(app.id);
|
const Operation* pending = findPendingOperation(app.id);
|
||||||
|
if (pending == nullptr && updatingAll && app.update_available) {
|
||||||
|
pending = pendingAppOperation;
|
||||||
|
}
|
||||||
app.operation_pending = pending != nullptr;
|
app.operation_pending = pending != nullptr;
|
||||||
if (pending != nullptr) {
|
if (pending != nullptr) {
|
||||||
app.pending_operation = pending->type;
|
app.pending_operation = pending->type;
|
||||||
@ -374,6 +384,41 @@ error_t executeInstall(const char* id) {
|
|||||||
return error;
|
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) {
|
error_t execute(const Operation& operation) {
|
||||||
switch (operation.type) {
|
switch (operation.type) {
|
||||||
case APP_HUB_OPERATION_REFRESH:
|
case APP_HUB_OPERATION_REFRESH:
|
||||||
@ -382,6 +427,8 @@ error_t execute(const Operation& operation) {
|
|||||||
return executeInstall(operation.app_id);
|
return executeInstall(operation.app_id);
|
||||||
case APP_HUB_OPERATION_REMOVE:
|
case APP_HUB_OPERATION_REMOVE:
|
||||||
return app_uninstall(operation.app_id);
|
return app_uninstall(operation.app_id);
|
||||||
|
case APP_HUB_OPERATION_UPDATE_ALL:
|
||||||
|
return executeUpdateAll(operation);
|
||||||
}
|
}
|
||||||
return ERROR_INVALID_ARGUMENT;
|
return ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
@ -417,14 +464,14 @@ int32_t workerMain(void* /*context*/) {
|
|||||||
queue.pop_front();
|
queue.pop_front();
|
||||||
operationRunning = true;
|
operationRunning = true;
|
||||||
const Operation operation = runningOperation;
|
const Operation operation = runningOperation;
|
||||||
emitEvent(getStartedEventType(operation.type), operation, ERROR_NONE);
|
emitEvent(getStartedEventType(operation.type), operation, operation.app_id, ERROR_NONE);
|
||||||
unlock();
|
unlock();
|
||||||
|
|
||||||
const error_t result = execute(operation);
|
const error_t result = execute(operation);
|
||||||
|
|
||||||
lock();
|
lock();
|
||||||
operationRunning = false;
|
operationRunning = false;
|
||||||
emitEvent(getFinishedEventType(operation.type), operation, result);
|
emitEvent(getFinishedEventType(operation.type), operation, operation.app_id, result);
|
||||||
unlock();
|
unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -611,6 +658,13 @@ error_t app_hub_get_description(const char* id, char* description, size_t descri
|
|||||||
|
|
||||||
// region Operations
|
// 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) {
|
error_t app_hub_refresh(uint32_t* operation_id) {
|
||||||
return enqueue(APP_HUB_OPERATION_REFRESH, nullptr, 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);
|
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) {
|
error_t app_hub_remove(const char* id, uint32_t* operation_id) {
|
||||||
return enqueue(APP_HUB_OPERATION_REMOVE, id, operation_id);
|
return enqueue(APP_HUB_OPERATION_REMOVE, id, operation_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include <app_hub/app_hub_service.h>
|
#include <app_hub/app_hub_service.h>
|
||||||
#include <app_hub/module.h>
|
#include <app_hub/module.h>
|
||||||
|
|
||||||
|
#include <app/manager.h>
|
||||||
#include <service/manager.h>
|
#include <service/manager.h>
|
||||||
|
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
@ -11,23 +12,37 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
static error_t start() {
|
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() {
|
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) {
|
if (error != ERROR_NONE && error != ERROR_NOT_FOUND) {
|
||||||
return error;
|
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[] = {
|
static const ModuleSymbol app_hub_module_symbols[] = {
|
||||||
DEFINE_MODULE_SYMBOL(app_hub_search),
|
DEFINE_MODULE_SYMBOL(app_hub_search),
|
||||||
DEFINE_MODULE_SYMBOL(app_hub_get_app),
|
DEFINE_MODULE_SYMBOL(app_hub_get_app),
|
||||||
DEFINE_MODULE_SYMBOL(app_hub_get_description),
|
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_refresh),
|
||||||
DEFINE_MODULE_SYMBOL(app_hub_install),
|
DEFINE_MODULE_SYMBOL(app_hub_install),
|
||||||
|
DEFINE_MODULE_SYMBOL(app_hub_update_all),
|
||||||
DEFINE_MODULE_SYMBOL(app_hub_remove),
|
DEFINE_MODULE_SYMBOL(app_hub_remove),
|
||||||
DEFINE_MODULE_SYMBOL(app_hub_event_subscribe),
|
DEFINE_MODULE_SYMBOL(app_hub_event_subscribe),
|
||||||
DEFINE_MODULE_SYMBOL(app_hub_event_unsubscribe),
|
DEFINE_MODULE_SYMBOL(app_hub_event_unsubscribe),
|
||||||
|
|||||||
@ -249,6 +249,28 @@ TEST_CASE("app_hub drops newer events while a subscriber's queue is full") {
|
|||||||
task_event_group_destruct(&group);
|
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") {
|
TEST_CASE("app_hub rejects invalid ids") {
|
||||||
ensureFixture();
|
ensureFixture();
|
||||||
CHECK_EQ(app_hub_install("", nullptr), ERROR_INVALID_ARGUMENT);
|
CHECK_EQ(app_hub_install("", nullptr), ERROR_INVALID_ARGUMENT);
|
||||||
|
|||||||
@ -159,6 +159,7 @@ shared_symbol_code_point_names = [
|
|||||||
"language", # Globe
|
"language", # Globe
|
||||||
"lists", # Chat app toolbar
|
"lists", # Chat app toolbar
|
||||||
"list_alt", # Settings apps: opens a selection dialog
|
"list_alt", # Settings apps: opens a selection dialog
|
||||||
|
"hourglass", # App Hub: an operation for the app is pending
|
||||||
"mail",
|
"mail",
|
||||||
"menu",
|
"menu",
|
||||||
"memory", # System Info
|
"memory", # System Info
|
||||||
|
|||||||
@ -9,7 +9,7 @@ extern "C" {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/** Changes whenever the icon TTF subset, its codepoints file or the icon name lists change */
|
/** 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
|
// Icon names per icon font, resolved through the .codepoints file next to the TTF
|
||||||
extern const char* const lvgl_icon_shared_names[];
|
extern const char* const lvgl_icon_shared_names[];
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
#define LVGL_ICON_SHARED_LANGUAGE "\xEE\xA8\x87"
|
#define LVGL_ICON_SHARED_LANGUAGE "\xEE\xA8\x87"
|
||||||
#define LVGL_ICON_SHARED_LISTS "\xEE\xA6\xB9"
|
#define LVGL_ICON_SHARED_LISTS "\xEE\xA6\xB9"
|
||||||
#define LVGL_ICON_SHARED_LIST_ALT "\xEE\x83\xAE"
|
#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_MAIL "\xEE\x85\x99"
|
||||||
#define LVGL_ICON_SHARED_MENU "\xEE\x97\x92"
|
#define LVGL_ICON_SHARED_MENU "\xEE\x97\x92"
|
||||||
#define LVGL_ICON_SHARED_MEMORY "\xEE\x8C\xA2"
|
#define LVGL_ICON_SHARED_MEMORY "\xEE\x8C\xA2"
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const char* const lvgl_icon_shared_names[] = {
|
|||||||
"language",
|
"language",
|
||||||
"lists",
|
"lists",
|
||||||
"list_alt",
|
"list_alt",
|
||||||
|
"hourglass",
|
||||||
"mail",
|
"mail",
|
||||||
"menu",
|
"menu",
|
||||||
"memory",
|
"memory",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#include <Tactility/Mutex.h>
|
#include <Tactility/Mutex.h>
|
||||||
|
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||||
#include <Tactility/app/apphubdetails/AppHubDetailsApp.h>
|
#include <Tactility/app/apphubdetails/AppHubDetailsApp.h>
|
||||||
|
|
||||||
#include <app/event.h>
|
#include <app/event.h>
|
||||||
@ -14,7 +15,9 @@
|
|||||||
#include <tactility/drivers/wifi.h>
|
#include <tactility/drivers/wifi.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
|
|
||||||
|
#include <lvgl/fonts.h>
|
||||||
#include <lvgl/grid_navigation.h>
|
#include <lvgl/grid_navigation.h>
|
||||||
|
#include <lvgl/icons/shared.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <lvgl/lvgl.h>
|
||||||
#include <lvgl/widgets/card.h>
|
#include <lvgl/widgets/card.h>
|
||||||
#include <lvgl/widgets/chip.h>
|
#include <lvgl/widgets/chip.h>
|
||||||
@ -50,6 +53,8 @@ struct Context {
|
|||||||
|
|
||||||
lv_obj_t* contentWrapper = nullptr;
|
lv_obj_t* contentWrapper = nullptr;
|
||||||
lv_obj_t* refreshButton = nullptr;
|
lv_obj_t* refreshButton = nullptr;
|
||||||
|
lv_obj_t* updateAllButton = nullptr;
|
||||||
|
lv_obj_t* updateAllSpinner = nullptr;
|
||||||
lv_obj_t* filterChips = nullptr;
|
lv_obj_t* filterChips = nullptr;
|
||||||
// Survives across a bury/resurface cycle
|
// Survives across a bury/resurface cycle
|
||||||
Filter filter = Filter::All;
|
Filter filter = Filter::All;
|
||||||
@ -74,6 +79,10 @@ struct Context {
|
|||||||
AppHubEventSubscription appHubSub {};
|
AppHubEventSubscription appHubSub {};
|
||||||
/** The id of the queued catalog refresh, 0 when none is pending */
|
/** The id of the queued catalog refresh, 0 when none is pending */
|
||||||
std::atomic<uint32_t> refreshOperationId {0};
|
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) {
|
void updateUpdatesChipText(Context* ctx, size_t updateCount) {
|
||||||
auto* chip = lv_obj_get_child(ctx->filterChips, static_cast<int32_t>(Filter::Updates));
|
auto* chip = lv_obj_get_child(ctx->filterChips, static_cast<int32_t>(Filter::Updates));
|
||||||
const auto text = std::format("{} {}", updateCount, updateCount == 1 ? "update" : "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_label_set_text(label, message);
|
||||||
|
|
||||||
lv_obj_set_hidden(ctx->refreshButton, false);
|
lv_obj_set_hidden(ctx->refreshButton, false);
|
||||||
|
updateToolbarActions(ctx, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showNoInternet(Context* ctx) {
|
void showNoInternet(Context* ctx) {
|
||||||
@ -236,6 +270,7 @@ void showApps(Context* ctx) {
|
|||||||
}, &apps);
|
}, &apps);
|
||||||
const bool hasApps = !apps.empty();
|
const bool hasApps = !apps.empty();
|
||||||
updateUpdatesChipText(ctx, std::ranges::count_if(apps, [](const AppHubApp& app) { return app.update_available; }));
|
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) {
|
std::erase_if(apps, [ctx](const AppHubApp& app) {
|
||||||
return !matchesFilter(app, ctx->filter);
|
return !matchesFilter(app, ctx->filter);
|
||||||
});
|
});
|
||||||
@ -256,8 +291,11 @@ void showApps(Context* ctx) {
|
|||||||
lv_obj_t* focus_target = nullptr;
|
lv_obj_t* focus_target = nullptr;
|
||||||
for (int i = 0; i < apps.size(); i++) {
|
for (int i = 0; i < apps.size(); i++) {
|
||||||
const auto& app = apps[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);
|
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);
|
auto int_as_voidptr = reinterpret_cast<void*>(i);
|
||||||
lv_obj_set_user_data(entry_button, int_as_voidptr);
|
lv_obj_set_user_data(entry_button, int_as_voidptr);
|
||||||
lv_obj_add_event_cb(entry_button, onAppPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
lv_obj_add_event_cb(entry_button, onAppPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||||
@ -331,7 +369,17 @@ void refresh(Context* ctx) {
|
|||||||
lvgl_unlock();
|
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;
|
const bool isRefresh = event.operation_id == ctx->refreshOperationId;
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
ctx->refreshOperationId = 0;
|
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();
|
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");
|
showRefreshFailedError(ctx, "Cannot reach server");
|
||||||
} else {
|
} else {
|
||||||
showApps(ctx);
|
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);
|
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);
|
ctx->refreshButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_REFRESH, onRefreshPressed, ctx);
|
||||||
lv_obj_set_hidden(ctx->refreshButton, true);
|
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);
|
ctx->filterChips = createFilterChips(parent, ctx);
|
||||||
updateFilterChips(ctx);
|
updateFilterChips(ctx);
|
||||||
@ -391,9 +440,36 @@ void destroyWidgets(void* userData) {
|
|||||||
ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
|
ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
|
||||||
ctx->contentWrapper = nullptr;
|
ctx->contentWrapper = nullptr;
|
||||||
ctx->refreshButton = nullptr;
|
ctx->refreshButton = nullptr;
|
||||||
|
ctx->updateAllButton = nullptr;
|
||||||
|
ctx->updateAllSpinner = nullptr;
|
||||||
ctx->filterChips = 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[]) {
|
int32_t appMain(int argc, char* argv[]) {
|
||||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||||
Context ctx;
|
Context ctx;
|
||||||
@ -429,11 +505,8 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
|
|
||||||
AppHubEvent app_hub_event {};
|
AppHubEvent app_hub_event {};
|
||||||
while (app_hub_event_poll(&ctx.appHubSub, &app_hub_event) == ERROR_NONE) {
|
while (app_hub_event_poll(&ctx.appHubSub, &app_hub_event) == ERROR_NONE) {
|
||||||
if (app_hub_event.type == APP_HUB_EVENT_REFRESHED ||
|
onUpdateAllEvent(&ctx, app_hub_event);
|
||||||
app_hub_event.type == APP_HUB_EVENT_INSTALLED ||
|
onOperationEvent(&ctx, app_hub_event);
|
||||||
app_hub_event.type == APP_HUB_EVENT_REMOVED) {
|
|
||||||
onOperationFinished(&ctx, app_hub_event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldClose && ctx.refreshRequested.exchange(false)) {
|
if (!shouldClose && ctx.refreshRequested.exchange(false)) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user