Refactor app loading and window management (#609)

This commit is contained in:
Ken Van Hoeylandt
2026-08-11 23:40:59 +02:00
committed by GitHub
parent dc3f6104b8
commit 37c507544b
243 changed files with 16034 additions and 10865 deletions
+103
View File
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Identifies the kind of app-lifecycle event delivered through app_event_await(). */
enum AppEventType {
APP_EVENT_RESULT, // struct AppResultEventData
APP_EVENT_CLOSE, // no data - terminate now, permanently
};
/** Data for APP_EVENT_RESULT. */
struct AppResultEventData {
uint32_t launch_id;
/** The child app instance's own AppMainFn/AppLoaderApi::run() return value. By convention:
* 0 = Ok, 1 = Cancelled, 2 = Error. Apps that need to hand back more than this (e.g. picked
* text, a path) expose their own "get last result" getter instead - see e.g.
* tt::app::inputdialog::getLastText(). */
int32_t result;
};
struct AppEvent {
enum AppEventType type;
/** Stamped by app_event_emit(); any value passed in by the caller is ignored. */
uint64_t timestamp;
/** Valid only when type == APP_EVENT_RESULT. */
struct AppResultEventData result;
};
/**
* Number of events that can be queued per subscription before app_event_emit() starts
* returning ERROR_RESOURCE (dropping the newest event, preserving FIFO order of what's
* already queued). Deliberately generous: app-module's scheduler is the only emitter and it
* serializes app-lifecycle transitions, so a given app can't realistically receive events
* faster than the scheduler produces them one at a time.
*/
#define APP_EVENT_QUEUE_CAPACITY 4
/**
* Caller-owned subscription node. Unlike TactilityKernel's system_event poll subscription
* (which coalesces to the latest value), this queues events by value (FIFO) since dropping an
* APP_EVENT_RESULT would be unacceptable.
* @warning Fields other than `app_instance_id` are for internal use only; do not read or write
* them directly.
*/
struct AppEventSubscription {
/** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */
uint32_t app_instance_id;
TaskHandle_t task;
struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY];
uint8_t head;
uint8_t count;
struct AppEventSubscription* next;
};
/**
* Register a subscription for events addressed to @a sub->app_instance_id.
* @warning Does not work in ISR context.
* @param[in,out] sub subscription to register; caller sets @a sub->app_instance_id beforehand,
* owns the storage, and must keep it alive (and stationary) until unsubscribed
* @return ERROR_NONE on success
*/
error_t app_event_subscribe(struct AppEventSubscription* sub);
/**
* Remove a previously registered subscription.
* @warning Does not work in ISR context.
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t app_event_unsubscribe(struct AppEventSubscription* sub);
/**
* Deliver @a event to every subscription registered for @a app_instance_id (normally exactly one).
* @warning Does not work in ISR context.
* @retval ERROR_NONE delivered to at least one subscription
* @retval ERROR_NOT_FOUND no subscription is registered for @a app_instance_id
* @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was
* dropped for that subscription (still delivered to any other matching subscription)
*/
error_t app_event_emit(uint32_t app_instance_id, const struct AppEvent* event);
/**
* Pop the next event for @a sub, blocking up to @a timeout if the queue is currently empty.
* @retval ERROR_NONE @a out_event was filled
* @retval ERROR_TIMEOUT no event arrived before the timeout elapsed
*/
error_t app_event_await(struct AppEventSubscription* sub, struct AppEvent* out_event, TickType_t timeout);
#ifdef __cplusplus
}
#endif
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Computes the install directory for @a app_id (does not check whether anything is actually
* installed there).
* @param[out] path always NULL-terminated on return, even on failure (empty string if
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
* NULL terminator)
* @retval ERROR_NOT_FOUND the app install location isn't available (e.g. no SD card)
*/
error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
/**
* Installs an app from a tarball at @a source_path: extracts it into the app install directory,
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
* If an app with the same id is already installed (via a previous app_install() call), it is
* uninstalled first - stopped if running, its old install directory removed - before the new
* one takes its place.
* @param[in] source_path path to a tar file containing the app (must have manifest.properties
* at its root)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read
* @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
*/
error_t app_install(const char* source_path);
/**
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
* install directory, and unregisters it (app_manager_remove()).
* @param[in] app_id the id the app was installed under (AppMetadata::app_id)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND no such app was installed via app_install()
*/
error_t app_uninstall(const char* app_id);
#ifdef __cplusplus
}
#endif
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Identifies a running (or previously running) app instance. 0 is never a valid instance id. */
typedef uint32_t AppInstanceId;
/** Lifecycle state of a running (or previously running) app instance. Every app instance owns
* its own task for its entire lifetime - there is no "saved, task given up" state. */
typedef enum {
APP_INSTANCE_STATE_STARTING,
APP_INSTANCE_STATE_ACTIVE,
APP_INSTANCE_STATE_STOPPING,
APP_INSTANCE_STATE_STOPPED,
} AppInstanceState;
#ifdef __cplusplus
}
#endif
+59
View File
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdint.h>
#include "location.h"
#ifdef __cplusplus
extern "C" {
#endif
/** service-module id the AppLoaderApi implementation for AppManifest::location.type ==
* APP_LOCATION_MEMORY must register under. Implemented by app-module itself (source/app_internal_loader.cpp). */
#define APP_LOADER_MEMORY_SERVICE_ID "app-loader-memory"
/** service-module id the AppLoaderApi implementation for AppManifest::location.type ==
* APP_LOCATION_PATH must register under. Implemented by a platform module (e.g. app-esp32-module). */
#define APP_LOADER_PATH_SERVICE_ID "app-loader-path"
/**
* Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this
* firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance,
* blocking for the app's whole lifetime - same contract as an external app's main(), plus
* @a app_instance_id identifying this running instance (use it with
* app_event_subscribe()/window_manager_create()/app_manager_finish()/etc.).
* AppManifest::location.location holds this cast to void*.
*/
typedef int32_t (*AppMainFn)(uint32_t app_instance_id, int argc, char* argv[]);
typedef void* AppRuntime;
/**
* Pluggable mechanism for loading and executing an app.
*/
struct AppLoaderApi {
/**
* Prepares an app instance for execution (e.g. read + relocate its binary).
* @param[in] location the location to load the elf from
* @param[out] out_runtime opaque handle to whatever load() allocated; passed back to run()/unload()
*/
error_t (*load)(struct AppLocation location, AppRuntime* out_runtime);
/**
* Blocking: runs the app to completion.
* @param[in] runtime handle produced by load()
* @param[in] app_instance_id the running instance's id
* @param[in] argc the amount of arguments in @a argv
* @param[in] argv the array of string pointers (can be NULL)
*/
int32_t (*run)(AppRuntime runtime, uint32_t app_instance_id, int argc, char* argv[]);
/** Releases whatever load() allocated. Called after run() returns. */
void (*unload)(AppRuntime runtime);
};
#ifdef __cplusplus
}
#endif
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
enum AppLocationType {
APP_LOCATION_MEMORY,
APP_LOCATION_PATH,
};
struct AppLocation {
enum AppLocationType type;
/** Meaning depends on `type`; see AppLocationType. */
void* location;
};
#ifdef __cplusplus
}
#endif
+153
View File
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#include <app/manifest.h>
#include <tactility/error.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Register an app manifest.
* @retval ERROR_INVALID_ARGUMENT a manifest with the same id is already registered
* @retval ERROR_NONE on success
*/
error_t app_manager_add(const struct AppManifest* manifest);
/**
* Unregister a previously-added manifest.
* @retval ERROR_NOT_FOUND no manifest with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_remove(const char* id);
/** @return the manifest, or NULL if not found. */
const struct AppManifest* app_manager_find_manifest(const char* id);
/**
* Calls `@a` visitor once for every registered manifest. Iteration order is unspecified.
* `@warning` `@a` visitor runs with app-module's internal registry lock held. Do not call any
* app_manager_*() function from inside `@a` visitor - copy out what you need and act on it after
* this call returns.
*/
typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context);
/**
* Starts a new instance of the app registered under @a id. Every app instance gets its own
* dedicated task for its entire lifetime - starting an app never asks any other app to give up
* its task, and multiple instances (of the same or different apps) can be Active at once.
* @param[in] id the manifest id to start
* @param[out] out_app_instance_id the id of the new app instance
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id);
/**
* Same as app_manager_start(), but also passes @a argc/@a argv to the new instance's own main
* function (see app/loader.h's AppMainFn) - modelled on a C program's main(argc, argv). For
* regular (non-modal) navigations that need to pass data to the target app (e.g. "show details
* for this app id") without expecting a result back.
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning, so
* @a argv and the strings it points to may be freed/go out of scope immediately after this call
* returns (e.g. safe to pass a stack-local array of a caller's own std::string::c_str()s).
*/
error_t app_manager_start_with_parameters(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Starts @a id as a modal child of @a parent_instance_id, for the purpose of receiving a
* result. The parent keeps running (window_manager's own multi-window stack handles burying its
* window while the child is shown).
*
* When the child's task exits, an APP_EVENT_RESULT is delivered to @a parent_instance_id -
* result is whatever the child's AppMainFn/AppLoaderApi::run() returned - unless
* @a parent_instance_id is 0, in which case no result is delivered (fire-and-forget, for
* callers with no app_instance_id of their own). The parent is then responsible for calling
* app_manager_stop() on the child's instance id to fully reap it. Children that need to hand
* back more than an int32_t (e.g. picked text, a path) expose their own "get last result"
* getter for the parent to call after receiving the event - see e.g.
* tt::app::inputdialog::getLastText().
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning (same as
* app_manager_start_with_parameters()), so @a argv and the strings it points to may be
* freed/go out of scope immediately after this call returns.
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_start_for_result(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit
* if it was running.
* @warning Must not be called from the instance's own task (it bound-waits via thread_join(),
* which asserts against joining yourself) - an app closing itself must call app_manager_finish()
* instead, right before returning from its own AppMainFn/AppLoaderApi::run().
*/
error_t app_manager_stop(AppInstanceId app_instance_id);
/**
* Called by an app instance, from its own task, right before it returns in response to
* APP_EVENT_CLOSE - whether that close was self-initiated (e.g. its own back button) or came
* from someone else. Marks this instance Stopped immediately (rather than waiting for its task
* to actually exit) so app_manager_get_state()/app_manager_get_topmost_instance_id() reflect the
* closure as soon as the app has decided to close, not just once its task has fully unwound.
* @warning Does not join or free this instance's own task/ledger entry (can't - this runs on
* that very task); those are cleaned up on a later app_manager_stop() call, same as any
* self-terminating instance.
*/
error_t app_manager_finish(AppInstanceId app_instance_id);
/** @return the instance's current state, or APP_INSTANCE_STATE_STOPPED if the id is unknown. */
AppInstanceState app_manager_get_state(AppInstanceId app_instance_id);
/**
* @param[out] out_app_instance_id set to the instance id of the topmost currently-Active app -
* the most recently started of whichever instances are Active (a modal child launched via
* app_manager_start_for_result() stays Active alongside its parent while shown, so this
* correctly picks the child, not the parent, while a dialog is up).
* @retval ERROR_NOT_FOUND no app is Active
* @retval ERROR_NONE on success
*/
error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id);
/**
* Same as app_manager_get_topmost_instance_id(), but resolves straight to the topmost app's
* manifest id string.
* @param[out] buffer always NULL-terminated on return, even on failure (empty string if
* @a buffer_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @retval ERROR_NOT_FOUND no app is Active
* @retval ERROR_BUFFER_OVERFLOW @a buffer_size is too small to hold the id (including the NULL
* terminator)
* @retval ERROR_NONE on success
*/
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
/**
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
* call app_manager_install_path_scan() to do that.
* @retval ERROR_NONE on success
*/
error_t app_manager_install_path_add(const char* path);
/**
* Scans every path registered via app_manager_install_path_add(): registers
* (app_manager_add()) any direct subdirectory with a valid manifest.properties that isn't
* already registered, and unregisters (app_manager_remove() only - does not stop it if running,
* does not delete anything) any manifest a previous scan registered whose directory has since
* disappeared. Safe to call repeatedly (e.g. after an SD card is mounted/unmounted).
*/
void app_manager_install_path_scan(void);
#ifdef __cplusplus
}
#endif
+41
View File
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "location.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory {
APP_CATEGORY_SYSTEM,
APP_CATEGORY_SETTINGS,
APP_CATEGORY_USER,
};
/** Bit flags for AppManifest::flags. */
enum AppManifestFlags {
/** Excluded from generic app-browsing UIs (AppList, Settings) - for apps only ever reached
* by direct navigation (modal dialogs, detail views that require parameters, wizard/
* bootstrap steps). */
APP_MANIFEST_FLAG_HIDDEN = 0b00000001,
};
/** Describes a registrable app. One manifest exists per app id. */
struct AppManifest {
/** Unique app identifier. Should never be NULL. */
const char* id;
/** Human-readable name. Should never be NULL. */
const char* name;
enum AppCategory category;
struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
uint8_t flags;
};
#ifdef __cplusplus
}
#endif
+60
View File
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define APP_METADATA_TARGET_SDK_LENGTH 16
#define APP_METADATA_APP_ID_LENGTH 32
#define APP_METADATA_APP_NAME_LENGTH 32
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
struct AppMetadata {
/**
* The SDK version that was used to compile this app. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[APP_METADATA_TARGET_SDK_LENGTH + 1];
/**
* The identifier by which the app is launched by the system and other apps.
* Must be NULL-terminated.
*/
char app_id[APP_METADATA_APP_ID_LENGTH + 1];
/**
* The user-readable name of the app. Used in UI.
* Must be NULL-terminated.
*/
char app_name[APP_METADATA_APP_NAME_LENGTH + 1];
/**
* The version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
/** The technical version (must be incremented with new releases of the app) */
uint64_t app_version_code;
};
/**
* Parses a manifest.properties file at @a path into @a out_metadata, auto-detecting the V1
* (sectioned, e.g. "[app]id=...") or V2 (flat dot-notation, e.g. "app.id=...") format from its
* first line.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* @a out_metadata's fixed-size buffers
*/
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata);
#ifdef __cplusplus
}
#endif
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module app_module;
#ifdef __cplusplus
}
#endif
+56
View File
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get the user data directory for an app. Survives OS upgrades. No trailing "/".
* @param[in] app_id non-null app id
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size);
/**
* @brief Get a path within the user data directory for an app.
* @param[in] app_id non-null app id
* @param[in] child_path path without a "/" prefix
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size);
/**
* @brief Get the assets directory for an app. Do not store configuration data here. No trailing "/".
* @param[in] app_id non-null app id
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size);
/**
* @brief Get a path within the assets directory for an app.
* @param[in] app_id non-null app id
* @param[in] child_path path without a "/" prefix
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @return the app_instance_id of whichever app instance's task is calling this (every app
* instance's task stashes it in its own thread-local storage when it starts), or 0 if called
* from a task that isn't a running app instance. An app's own main() typically calls this once,
* near the top, to learn its own instance id - see e.g. app_event_subscribe()/
* window_manager_create(), both of which need it.
*/
AppInstanceId app_scheduler_current_app_id(void);
#ifdef __cplusplus
}
#endif