Merge develop into main (#348)

## App state

Improved app state management in `LoaderService` and `GuiService`:

- Re-ordered some of the state transitions
- Hardened `GuiService` for repeated events (that might trigger a re-render of an app that's already rendered)
- Validate state transitions in `LoaderService` and crash if an app transitions from the wrong state to the next one.

## LoaderService

- Removed `tt::loader::` functions and expose `LoaderService` interface publicly.
- Implement `stopAll()` and `stopAll(id)` which stops all instances of an app, including any apps that were launched by it.
- Rename `stop()` functions to `stopTop()`
- Created `stopTop(id)` which only stops the top-most app when the app id matches.
- Moved `loader::LoaderEvent` to `loader::LoaderService::Event`
- Changed app instance `std::stack` to `std::vector`

## Improvements

- `ElfApp`: error 22 now shows a hint that `main()` might be missing
- Starting, installing and uninstalling apps now stops any running app (and its children) on the stack

## Bugfixes

- `HttpdReq` out of memory issue now shows an error message and doesn't crash anymore (this would happen on devices without PSRAM with WiFi active, when an app was installed)
- `GuiService::hideApp()` lock should not wait for timeout and now waits indefinitely
- `Buildscript/release-sdk-current.sh` deletes the previous local release before building a new one

## Code correctness

- App classes were made `final`
- Apps that had a `void start()` now have a `LaunchId start()`
- `tt::app::State`: renamed `Started` to `Created` and `Stopped` to `Destroyed` to properly reflect earlier name changes
This commit is contained in:
Ken Van Hoeylandt 2025-09-27 18:04:09 +02:00 committed by GitHub
parent dcf28d0868
commit f6cdabf3c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 504 additions and 342 deletions

View File

@ -1,8 +1,9 @@
#include "UnPhoneFeatures.h"
#include <Tactility/app/App.h>
#include <Tactility/Log.h>
#include <Tactility/kernel/Kernel.h>
#include <Tactility/service/loader/Loader.h>
#include <driver/gpio.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
@ -43,7 +44,7 @@ static int32_t buttonHandlingThreadMain(void* context) {
// The buttons might generate more than 1 click because of how they are built
TT_LOG_I(TAG, "Pressed button %d", pinNumber);
if (pinNumber == pin::BUTTON1) {
tt::service::loader::stopApp();
tt::app::stop();
}
// Debounce all events for a short period of time

View File

@ -5,5 +5,6 @@
# This deployment is used when compiling apps in ./ExternalApps
#
rm -rf release/TactilitySDK
./Buildscripts/release-sdk.sh release/TactilitySDK

View File

@ -2,14 +2,12 @@
## Higher Priority
- Show a warning in the web installer when flashing CYD 28R board regarding v1/v2/v3
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility.
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility
Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update)
- Make a URL handler. Use it for handling local files. Match file types with apps.
Create some kind of "intent" handler like on Android.
The intent can have an action (e.g. view), a URL and an optional bundle.
The manifest can provide the intent handler
- CrowPanel Basic 3.5": check why GraphicsDemo fails
- CrowPanel Basic 3.5": check why System Info doesn't show storage info
- When an SD card is detected, check if it has been initialized and assigned as data partition.
If the user choses to select it, then copy files from /data over to it.
Write the user choice to a file on the card.
@ -17,14 +15,6 @@
The latter is used for auto-selecting it as data partition.
- Support direct installation of an `.app` file with `tactility.py install helloworld.app <ip>`
- Support `tactility.py target <ip>` to remember the device IP address.
- External app error code 22 should warn that the user might've forgotten a `main()` entry point
- Bug: `Buildscript/release-sdk-current.sh` should delete the currently released SDK. It should probably also output it with versioning and target platform naming so it can be referred to as if it is a real release.
- Tactility docs: external app dev guide should explain [debugging](https://docs.zephyrproject.org/latest/services/llext/debug.html)
- elf_loader changes/suggestions:
- Make entry-point optional (so we can build libraries, or have the `manifest` as a global symbol)
- Implement support for alternative symbol lists. e.g. a function pointer that resolves a single symbol.
- Implement the entire list of [soft-float library functions](https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html) to `tt_init.cpp`
- `tactility.py` should stop running applications when it is: uninstalling, installing, or running an application that is already running.
## Medium Priority
@ -40,9 +30,11 @@
- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device)
- I2C app should show error when I2C port is disabled when the scan button was manually pressed
- TactilitySDK: Support automatic scanning of header files so that we can generate the `tt_init.cpp` symbols list.
- elf_loader: split up symbol lists further (after radio support is implemented)
## Lower Priority
- elf_loader: make main() entry-point optional (so we can build libraries, or have the `manifest` as a global symbol)
- Implement system suspend that turns off the screen
- The boot button on some devices can be used as GPIO_NUM_0 at runtime
- Localize all apps

View File

@ -88,6 +88,20 @@ LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable pa
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stop();
/** @brief Stop a specific app and any apps it might have launched on the stack.
* @param[in] id the app id
*/
void stop(const std::string& id);
/** @brief Stop all app instances that match with this identifier and also stop the apps they started.
* @warning onResult() will only be called for the resulting app that gets shown (if any)
* @param[in] id the id of the app to stop
*/
void stopAll(const std::string& id);
/** @return true if the app is running somewhere in the app stack (doesn't have to be the top-most app) */
bool isRunning(const std::string& id);
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
std::shared_ptr<AppContext> _Nullable getCurrentAppContext();

View File

@ -1,7 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::imageviewer {
void start(const std::string& file);
LaunchId start(const std::string& file);
};
}

View File

@ -1,5 +1,6 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include <string>
@ -11,7 +12,7 @@
*/
namespace tt::app::inputdialog {
void start(const std::string& title, const std::string& message, const std::string& prefilled = "");
LaunchId start(const std::string& title, const std::string& message, const std::string& prefilled = "");
/**
* @return the text that was in the field when OK was pressed, or otherwise empty string

View File

@ -1,11 +1,14 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::notes {
/**
* Start the notes app with the specified text file.
* @param[in] filePath the path to the text file to open
* @return the launch id
*/
void start(const std::string& filePath);
LaunchId start(const std::string& filePath);
}

View File

@ -1,5 +1,6 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include <string>
@ -15,7 +16,7 @@
*/
namespace tt::app::selectiondialog {
void start(const std::string& title, const std::vector<std::string>& items);
LaunchId start(const std::string& title, const std::vector<std::string>& items);
/**
* Get the index of the item that the user selected.

View File

@ -1,7 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::wifimanage {
void start();
LaunchId start();
} // namespace

View File

@ -1,45 +1,99 @@
#pragma once
#include "Tactility/app/AppManifest.h"
#include <Tactility/app/AppInstance.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/Bundle.h>
#include <Tactility/DispatcherThread.h>
#include <Tactility/PubSub.h>
#include <Tactility/service/Service.h>
#include <memory>
namespace tt::service::loader {
// region LoaderEvent for PubSub
enum class LoaderEvent{
ApplicationStarted,
ApplicationShowing,
ApplicationHiding,
ApplicationStopped
class LoaderService final : public Service {
public:
enum class Event {
ApplicationStarted,
ApplicationShowing,
ApplicationHiding,
ApplicationStopped
};
private:
std::shared_ptr<PubSub<Event>> pubsubExternal = std::make_shared<PubSub<Event>>();
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::vector<std::shared_ptr<app::AppInstance>> appStack;
app::LaunchId nextLaunchId = 0;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw()
*/
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters);
void onStopTopAppMessage(const std::string& id);
void onStopAllAppMessage(const std::string& id);
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
int findAppInStack(const std::string& id) const;
bool onStart(TT_UNUSED ServiceContext& service) override {
dispatcherThread->start();
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
// Send stop signal to thread and wait for thread to finish
mutex.withLock([this] {
dispatcherThread->stop();
});
}
public:
/**
* @brief Start an app given an app id and an optional bundle with parameters
* @param id the app identifier
* @param parameters optional parameter bundle
* @return the launch id
*/
app::LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters);
/**
* @brief Stops the top-most app (the one that is currently active shown to the user
* @warning Avoid calling this directly and use stopTop(id) instead
*/
void stopTop();
/**
* @brief Stops the top-most app if the id is still matching by the time the stop event arrives.
* @param id the id of the app to stop
*/
void stopTop(const std::string& id);
/**
* @brief Stops all apps with the provided id and any apps that were pushed on top of the stack after the original app was started.
* @param id the id of the app to stop
*/
void stopAll(const std::string& id);
/** @return the AppContext of the top-most application */
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
/** @return true if the app is running anywhere in the app stack (the app does not have to be the top-most one for this to return true) */
bool isRunning(const std::string& id) const;
/** @return the PubSub object that is responsible for event publishing */
std::shared_ptr<PubSub<Event>> getPubsub() const { return pubsubExternal; }
};
// endregion LoaderEvent for PubSub
/**
* @brief Start an app
* @param[in] id application name or id
* @param[in] parameters optional parameters to pass onto the application
* @return the launch id
*/
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stopApp();
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
/** @return the currently running app (it is only ever null before the splash screen is shown) */
std::shared_ptr<app::App> _Nullable getCurrentApp();
/**
* @brief PubSub for LoaderEvent
*/
std::shared_ptr<PubSub<LoaderEvent>> getPubsub();
std::shared_ptr<LoaderService> _Nullable findLoaderService();
} // namespace

View File

@ -13,11 +13,11 @@
namespace tt::app {
enum class State {
Initial, // App is being activated in loader
Started, // App is in memory
Showing, // App view is created
Hiding, // App view is destroyed
Stopped // App is not in memory
Initial, // AppInstance was created, but the state hasn't advanced yet
Created, // App was placed into memory
Showing, // App view was created
Hiding, // App view was destroyed
Destroyed // App was removed from memory
};
/**

View File

@ -1,7 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::i2cscanner {
void start();
LaunchId start();
}

View File

@ -1,7 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::launcher {
void start();
LaunchId start();
}

View File

@ -1,7 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::localesettings {
void start();
LaunchId start();
}

View File

@ -1,7 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::timedatesettings {
void start();
LaunchId start();
}

View File

@ -1,10 +1,11 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
namespace tt::app::timezone {
void start();
LaunchId start();
std::string getResultName(const Bundle& bundle);
std::string getResultCode(const Bundle& bundle);

View File

@ -45,7 +45,7 @@ public:
/**
* Start the app with optional pre-filled fields.
*/
void start(const std::string& ssid = "", const std::string& password = "");
LaunchId start(const std::string& ssid = "", const std::string& password = "");
bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid);

View File

@ -1,7 +1,7 @@
#pragma once
#ifdef ESP_PLATFORM
#include "Tactility/service/Service.h"
#include <Tactility/service/Service.h>
#include <Tactility/Mutex.h>

View File

@ -17,19 +17,19 @@ namespace tt::service::gui {
#define GUI_THREAD_FLAG_EXIT (1 << 2)
#define GUI_THREAD_FLAG_ALL (GUI_THREAD_FLAG_DRAW | GUI_THREAD_FLAG_INPUT | GUI_THREAD_FLAG_EXIT)
class GuiService : public Service {
class GuiService final : public Service {
// Thread and lock
Thread* thread = nullptr;
Mutex mutex = Mutex(Mutex::Type::Recursive);
PubSub<loader::LoaderEvent>::SubscriptionHandle loader_pubsub_subscription = nullptr;
PubSub<loader::LoaderService::Event>::SubscriptionHandle loader_pubsub_subscription = nullptr;
// Layers and Canvas
lv_obj_t* appRootWidget = nullptr;
lv_obj_t* statusbarWidget = nullptr;
// App-specific
std::shared_ptr<app::AppContext> appToRender = nullptr;
std::shared_ptr<app::AppInstance> appToRender = nullptr;
lv_obj_t* _Nullable keyboard = nullptr;
lv_group_t* keyboardGroup = nullptr;
@ -38,7 +38,7 @@ class GuiService : public Service {
static int32_t guiMain();
void onLoaderEvent(loader::LoaderEvent event);
void onLoaderEvent(loader::LoaderService::Event event);
lv_obj_t* createAppViews(lv_obj_t* parent);
@ -52,7 +52,7 @@ class GuiService : public Service {
tt_check(mutex.unlock());
}
void showApp(std::shared_ptr<app::AppContext> app);
void showApp(std::shared_ptr<app::AppInstance> app);
void hideApp();

View File

@ -284,7 +284,7 @@ void run(const Configuration& config) {
TT_LOG_I(TAG, "Starting boot app");
// The boot app takes care of registering system apps, user services and user apps
addApp(app::boot::manifest);
service::loader::startApp(app::boot::manifest.appId);
app::start(app::boot::manifest.appId);
TT_LOG_I(TAG, "Main dispatcher ready");
while (true) {

View File

@ -3,20 +3,47 @@
namespace tt::app {
constexpr auto* TAG = "App";
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
return service::loader::startApp(id, std::move(parameters));
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
return service->start(id, std::move(parameters));
}
void stop() {
service::loader::stopApp();
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
service->stopTop();
}
void stop(const std::string& id) {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
service->stopTop(id);
}
void stopAll(const std::string& id) {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
service->stopAll(id);
}
bool isRunning(const std::string& id) {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
return service->isRunning(id);
}
std::shared_ptr<AppContext> _Nullable getCurrentAppContext() {
return service::loader::getCurrentAppContext();
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
return service->getCurrentAppContext();
}
std::shared_ptr<App> _Nullable getCurrentApp() {
return service::loader::getCurrentApp();
const auto app_context = getCurrentAppContext();
return (app_context != nullptr) ? app_context->getApp() : nullptr;
}
}

View File

@ -174,6 +174,11 @@ bool install(const std::string& path) {
return false;
}
// If the app was already running, then stop it
if (isRunning(manifest.appId)) {
stopAll(manifest.appId);
}
target_path_lock.lock();
const std::string renamed_target_path = std::format("{}/{}", app_parent_path, manifest.appId);
if (file::isDirectory(renamed_target_path)) {
@ -202,6 +207,12 @@ bool install(const std::string& path) {
bool uninstall(const std::string& appId) {
TT_LOG_I(TAG, "Uninstalling app %s", appId.c_str());
// If the app was running, then stop it
if (isRunning(appId)) {
stopAll(appId);
}
auto app_path = getAppInstallPath(appId);
return file::withLock<bool>(app_path, [&app_path, &appId] {
if (!file::isDirectory(app_path)) {

View File

@ -23,6 +23,8 @@ static std::string getErrorCodeString(int error_code) {
return "out of memory";
case ENOSYS:
return "missing symbol";
case EINVAL:
return "invalid argument or main() missing";
default:
return std::format("code {}", error_code);
}
@ -137,14 +139,14 @@ public:
staticParametersSetCount = 0;
if (!startElf()) {
service::loader::stopApp();
stop();
auto message = lastError.empty() ? "Application failed to start." : std::format("Application failed to start: {}", lastError);
alertdialog::start("Error", message);
return;
}
if (staticParametersSetCount == 0) {
service::loader::stopApp();
stop();
alertdialog::start("Error", "Application failed to start: application failed to register itself");
return;
}

View File

@ -28,7 +28,7 @@ LaunchId start(const std::string& title, const std::string& message, const std::
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined);
return service::loader::startApp(manifest.appId, bundle);
return app::start(manifest.appId, bundle);
}
LaunchId start(const std::string& title, const std::string& message) {
@ -36,7 +36,7 @@ LaunchId start(const std::string& title, const std::string& message) {
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK");
return service::loader::startApp(manifest.appId, bundle);
return app::start(manifest.appId, bundle);
}
int32_t getResultIndex(const Bundle& bundle) {
@ -72,9 +72,9 @@ private:
auto bundle = std::make_unique<Bundle>();
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
setResult(app::Result::Ok, std::move(bundle));
setResult(Result::Ok, std::move(bundle));
service::loader::stopApp();
stop(manifest.appId);
}
static void createButton(lv_obj_t* parent, const std::string& text, size_t index) {

View File

@ -1,6 +1,6 @@
#include "Tactility/app/AppRegistration.h"
#include "Tactility/service/loader/Loader.h"
#include "Tactility/lvgl/Toolbar.h"
#include <Tactility/app/AppRegistration.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Assets.h>
@ -9,11 +9,11 @@
namespace tt::app::applist {
class AppListApp : public App {
class AppListApp final : public App {
static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->appId);
start(manifest->appId);
}
static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {

View File

@ -24,6 +24,7 @@
namespace tt::app::boot {
constexpr auto* TAG = "Boot";
extern const AppManifest manifest;
static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
@ -107,7 +108,7 @@ class BootApp : public App {
TT_LOG_I(TAG, "initFromBootApp");
initFromBootApp();
waitForMinimalSplashDuration(start_time);
service::loader::stopApp();
stop(manifest.appId);
startNextApp();
}
@ -128,7 +129,7 @@ class BootApp : public App {
return;
}
service::loader::startApp(boot_properties.launcherAppId);
start(boot_properties.launcherAppId);
}
static int getSmallestDimension() {

View File

@ -15,8 +15,10 @@
namespace tt::app::crashdiagnostics {
extern const AppManifest manifest;
void onContinuePressed(TT_UNUSED lv_event_t* event) {
service::loader::stopApp();
stop(manifest.appId);
launcher::start();
}
@ -48,7 +50,7 @@ public:
int qr_version;
if (!getQrVersionForBinaryDataLength(url_length, qr_version)) {
TT_LOG_E(TAG, "QR is too large");
service::loader::stopApp();
stop(manifest.appId);
return;
}
@ -56,7 +58,7 @@ public:
auto qrcodeData = std::make_shared<uint8_t[]>(qrcode_getBufferSize(qr_version));
if (qrcodeData == nullptr) {
TT_LOG_E(TAG, "Failed to allocate QR buffer");
service::loader::stopApp();
stop();
return;
}
@ -64,7 +66,7 @@ public:
TT_LOG_I(TAG, "QR init text");
if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) {
TT_LOG_E(TAG, "QR init text failed");
service::loader::stopApp();
stop(manifest.appId);
return;
}
@ -84,7 +86,7 @@ public:
pixel_size = 1;
} else {
TT_LOG_E(TAG, "QR code won't fit screen");
service::loader::stopApp();
stop(manifest.appId);
return;
}
@ -99,7 +101,7 @@ public:
auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO);
if (draw_buf == nullptr) {
TT_LOG_E(TAG, "Draw buffer alloc");
service::loader::stopApp();
stop(manifest.appId);
return;
}
@ -130,7 +132,7 @@ extern const AppManifest manifest = {
};
void start() {
service::loader::startApp(manifest.appId);
app::start(manifest.appId);
}
} // namespace

View File

@ -1,10 +1,7 @@
#ifdef ESP_PLATFORM
#include "Tactility/lvgl/Lvgl.h"
#include <Tactility/Tactility.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
@ -12,6 +9,7 @@
#include <Tactility/service/development/DevelopmentSettings.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/Tactility.h>
#include <Tactility/Timer.h>
#include <cstring>
@ -20,6 +18,7 @@
namespace tt::app::development {
constexpr const char* TAG = "Development";
extern const AppManifest manifest;
class DevelopmentApp final : public App {
@ -85,7 +84,7 @@ public:
service = service::development::findService();
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found");
service::loader::stopApp();
stop(manifest.appId);
}
}

View File

@ -42,7 +42,7 @@ extern const AppManifest manifest = {
};
void start() {
service::loader::startApp(manifest.appId);
app::start(manifest.appId);
}
} // namespace

View File

@ -45,7 +45,7 @@ public:
auto bundle = std::make_unique<Bundle>();
bundle->putString("path", path);
setResult(Result::Ok, std::move(bundle));
service::loader::stopApp();
stop(manifest.appId);
});
}
@ -67,13 +67,13 @@ extern const AppManifest manifest = {
LaunchId startForExistingFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::Existing);
return service::loader::startApp(manifest.appId, bundle);
return start(manifest.appId, bundle);
}
LaunchId startForExistingOrNewFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::ExistingOrNew);
return service::loader::startApp(manifest.appId, bundle);
return start(manifest.appId, bundle);
}
} // namespace

View File

@ -410,8 +410,8 @@ extern const AppManifest manifest = {
.createApp = create<I2cScannerApp>
};
void start() {
service::loader::startApp(manifest.appId);
LaunchId start() {
return app::start(manifest.appId);
}
} // namespace

View File

@ -1,7 +1,6 @@
#include "Tactility/lvgl/Style.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/TactilityCore.h>
#include <Tactility/StringUtils.h>
@ -11,10 +10,10 @@ namespace tt::app::imageviewer {
extern const AppManifest manifest;
#define TAG "image_viewer"
#define IMAGE_VIEWER_FILE_ARGUMENT "file"
constexpr auto* TAG = "ImageViewer";
constexpr auto* IMAGE_VIEWER_FILE_ARGUMENT = "file";
class ImageViewerApp : public App {
class ImageViewerApp final : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
auto wrapper = lv_obj_create(parent);
@ -67,10 +66,10 @@ extern const AppManifest manifest = {
.createApp = create<ImageViewerApp>
};
void start(const std::string& file) {
LaunchId start(const std::string& file) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(IMAGE_VIEWER_FILE_ARGUMENT, file);
service::loader::startApp(manifest.appId, parameters);
return app::start(manifest.appId, parameters);
}
} // namespace

View File

@ -1,32 +1,31 @@
#include "Tactility/app/inputdialog/InputDialog.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/app/inputdialog/InputDialog.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/TactilityCore.h>
#include <lvgl.h>
namespace tt::app::inputdialog {
#define PARAMETER_BUNDLE_KEY_TITLE "title"
#define PARAMETER_BUNDLE_KEY_MESSAGE "message"
#define PARAMETER_BUNDLE_KEY_PREFILLED "prefilled"
#define RESULT_BUNDLE_KEY_RESULT "result"
constexpr auto* PARAMETER_BUNDLE_KEY_TITLE = "title";
constexpr auto* PARAMETER_BUNDLE_KEY_MESSAGE = "message";
constexpr auto* PARAMETER_BUNDLE_KEY_PREFILLED = "prefilled";
constexpr auto* RESULT_BUNDLE_KEY_RESULT = "result";
#define DEFAULT_TITLE "Input"
constexpr auto* DEFAULT_TITLE = "Input";
#define TAG "input_dialog"
constexpr auto* TAG = "InputDialog";
extern const AppManifest manifest;
class InputDialogApp;
void start(const std::string& title, const std::string& message, const std::string& prefilled) {
LaunchId start(const std::string& title, const std::string& message, const std::string& prefilled) {
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled);
service::loader::startApp(manifest.appId, bundle);
return app::start(manifest.appId, bundle);
}
std::string getResult(const Bundle& bundle) {
@ -44,7 +43,7 @@ static std::string getTitleParameter(const std::shared_ptr<const Bundle>& bundle
}
}
class InputDialogApp : public App {
class InputDialogApp final : public App {
static void createButton(lv_obj_t* parent, const std::string& text, void* callbackContext) {
lv_obj_t* button = lv_button_create(parent);
@ -73,7 +72,7 @@ class InputDialogApp : public App {
setResult(Result::Cancelled);
}
service::loader::stopApp();
stop(manifest.appId);
}
public:

View File

@ -60,7 +60,7 @@ class LauncherApp final : public App {
static void onAppPressed(TT_UNUSED lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
service::loader::startApp(appId);
start(appId);
}
static void onPowerOffPressed(lv_event_t* e) {
@ -76,7 +76,7 @@ public:
settings::BootSettings boot_properties;
if (settings::loadBootSettings(boot_properties) && !boot_properties.autoStartAppId.empty()) {
TT_LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
service::loader::startApp(boot_properties.autoStartAppId);
start(boot_properties.autoStartAppId);
}
}
@ -144,8 +144,8 @@ extern const AppManifest manifest = {
.createApp = create<LauncherApp>
};
void start() {
service::loader::startApp(manifest.appId);
LaunchId start() {
return app::start(manifest.appId);
}
} // namespace

View File

@ -5,12 +5,12 @@
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/Time.h>
#include <Tactility/StringUtils.h>
#include <Tactility/settings/Language.h>
#include <lvgl.h>
#include <map>
#include <sstream>
#include <Tactility/StringUtils.h>
#include <Tactility/settings/Language.h>
namespace tt::app::localesettings {
@ -18,7 +18,7 @@ constexpr auto* TAG = "LocaleSettings";
extern const AppManifest manifest;
class LocaleSettingsApp : public App {
class LocaleSettingsApp final : public App {
tt::i18n::TextResources textResources = tt::i18n::TextResources("/system/app/LocaleSettings/i18n");
Mutex mutex = Mutex(Mutex::Type::Recursive);
lv_obj_t* timeZoneLabel = nullptr;
@ -169,8 +169,8 @@ extern const AppManifest manifest = {
.createApp = create<LocaleSettingsApp>
};
void start() {
service::loader::startApp(manifest.appId);
LaunchId start() {
return app::start(manifest.appId);
}
} // namespace

View File

@ -1,10 +1,10 @@
#include "Tactility/app/AppManifest.h"
#include "Tactility/app/fileselection/FileSelection.h"
#include "Tactility/file/FileLock.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/service/loader/Loader.h"
#include "Tactility/Assets.h"
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/file/FileLock.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/Assets.h>
#include <Tactility/file/File.h>
@ -15,7 +15,7 @@ namespace tt::app::notes {
constexpr auto* TAG = "Notes";
constexpr auto* NOTES_FILE_ARGUMENT = "file";
class NotesApp : public App {
class NotesApp final : public App {
lv_obj_t* uiCurrentFileName;
lv_obj_t* uiDropDownMenu;
@ -213,9 +213,10 @@ extern const AppManifest manifest = {
.createApp = create<NotesApp>
};
void start(const std::string& filePath) {
LaunchId start(const std::string& filePath) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(NOTES_FILE_ARGUMENT, filePath);
service::loader::startApp(manifest.appId, parameters);
return app::start(manifest.appId, parameters);
}
} // namespace tt::app::notes

View File

@ -1,8 +1,7 @@
#include "Tactility/app/selectiondialog/SelectionDialog.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/app/selectiondialog/SelectionDialog.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityCore.h>
@ -10,23 +9,23 @@
namespace tt::app::selectiondialog {
#define PARAMETER_BUNDLE_KEY_TITLE "title"
#define PARAMETER_BUNDLE_KEY_ITEMS "items"
#define RESULT_BUNDLE_KEY_INDEX "index"
constexpr auto* PARAMETER_BUNDLE_KEY_TITLE = "title";
constexpr auto* PARAMETER_BUNDLE_KEY_ITEMS = "items";
constexpr auto* RESULT_BUNDLE_KEY_INDEX = "index";
#define PARAMETER_ITEM_CONCATENATION_TOKEN ";;"
#define DEFAULT_TITLE "Select..."
constexpr auto* PARAMETER_ITEM_CONCATENATION_TOKEN = ";;";
constexpr auto* DEFAULT_TITLE = "Select...";
#define TAG "selection_dialog"
constexpr auto* TAG = "SelectionDialog";
extern const AppManifest manifest;
void start(const std::string& title, const std::vector<std::string>& items) {
LaunchId start(const std::string& title, const std::vector<std::string>& items) {
std::string items_joined = string::join(items, PARAMETER_ITEM_CONCATENATION_TOKEN);
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_ITEMS, items_joined);
service::loader::startApp(manifest.appId, bundle);
return app::start(manifest.appId, bundle);
}
int32_t getResultIndex(const Bundle& bundle) {
@ -44,9 +43,7 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
}
}
class SelectionDialogApp : public App {
private:
class SelectionDialogApp final : public App {
static void onListItemSelectedCallback(lv_event_t* e) {
auto app = std::static_pointer_cast<SelectionDialogApp>(getCurrentApp());
@ -59,8 +56,8 @@ private:
TT_LOG_I(TAG, "Selected item at index %d", index);
auto bundle = std::make_unique<Bundle>();
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
setResult(app::Result::Ok, std::move(bundle));
service::loader::stopApp();
setResult(Result::Ok, std::move(bundle));
stop(manifest.appId);
}
static void createChoiceItem(void* parent, const std::string& title, size_t index) {
@ -90,12 +87,12 @@ public:
if (items.empty() || items.front().empty()) {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
service::loader::stopApp();
stop(manifest.appId);
} else if (items.size() == 1) {
auto result_bundle = std::make_unique<Bundle>();
result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0);
setResult(Result::Ok, std::move(result_bundle));
service::loader::stopApp();
stop(manifest.appId);
TT_LOG_W(TAG, "Auto-selecting single item");
} else {
size_t index = 0;
@ -106,7 +103,7 @@ public:
} else {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
service::loader::stopApp();
stop(manifest.appId);
}
}
};

View File

@ -1,6 +1,6 @@
#include "Tactility/app/AppRegistration.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/app/AppRegistration.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/Assets.h>
#include <Tactility/Check.h>
@ -12,7 +12,7 @@ namespace tt::app::settings {
static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->appId);
start(manifest->appId);
}
static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* parent) {
@ -23,7 +23,7 @@ static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* par
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get());
}
class SettingsApp : public App {
class SettingsApp final : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
@ -36,7 +36,7 @@ class SettingsApp : public App {
lv_obj_set_flex_grow(list, 1);
auto manifests = getApps();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
std::ranges::sort(manifests, SortAppManifestByName);
for (const auto& manifest: manifests) {
if (manifest->appCategory == Category::Settings) {
createWidget(manifest, list);

View File

@ -12,7 +12,7 @@ constexpr auto* TAG = "TimeDate";
extern const AppManifest manifest;
class TimeDateSettingsApp : public App {
class TimeDateSettingsApp final : public App {
Mutex mutex = Mutex(Mutex::Type::Recursive);
@ -64,8 +64,8 @@ extern const AppManifest manifest = {
.createApp = create<TimeDateSettingsApp>
};
void start() {
service::loader::startApp(manifest.appId);
LaunchId start() {
return app::start(manifest.appId);
}
} // namespace

View File

@ -104,8 +104,7 @@ class TimeZoneApp final : public App {
setResultCode(*bundle, entry.code);
setResult(Result::Ok, std::move(bundle));
service::loader::stopApp();
stop(manifest.appId);
}
static void createListItem(lv_obj_t* list, const std::string& title, size_t index) {
@ -234,8 +233,8 @@ extern const AppManifest manifest = {
.createApp = create<TimeZoneApp>
};
void start() {
service::loader::startApp(manifest.appId);
LaunchId start() {
return app::start(manifest.appId);
}
}

View File

@ -33,7 +33,7 @@ void WifiConnect::onWifiEvent(service::wifi::WifiEvent event) {
case service::wifi::WifiEvent::ConnectionSuccess:
if (getState().isConnecting()) {
state.setConnecting(false);
service::loader::stopApp();
stop(manifest.appId);
}
break;
default:
@ -102,11 +102,11 @@ extern const AppManifest manifest = {
.createApp = create<WifiConnect>
};
void start(const std::string& ssid, const std::string& password) {
LaunchId start(const std::string& ssid, const std::string& password) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(WIFI_CONNECT_PARAM_SSID, ssid);
parameters->putString(WIFI_CONNECT_PARAM_PASSWORD, password);
service::loader::startApp(manifest.appId, parameters);
return app::start(manifest.appId, parameters);
}
bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid) {

View File

@ -140,8 +140,8 @@ extern const AppManifest manifest = {
.createApp = create<WifiManage>
};
void start() {
service::loader::startApp(manifest.appId);
LaunchId start() {
return app::start(manifest.appId);
}
} // namespace

View File

@ -65,7 +65,7 @@ static const lv_obj_class_t toolbar_class = {
};
static void stop_app(TT_UNUSED lv_event_t* event) {
service::loader::stopApp();
app::stop();
}
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {

View File

@ -1,4 +1,4 @@
#include "Tactility/network/HttpdReq.h"
#include <Tactility/network/HttpdReq.h>
#include <memory>
#include <ranges>
@ -8,10 +8,10 @@
#ifdef ESP_PLATFORM
#define TAG "network"
namespace tt::network {
constexpr auto* TAG = "HttpdReq";
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) {
size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str());
if (header_size == 0) {
@ -73,11 +73,17 @@ std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, si
assert(length > 0);
bytesRead = 0;
auto result = std::make_unique<char[]>(length);
// We have to use malloc() because make_unique() throws an exception
// and we don't have exceptions enabled in the compiler settings
auto* buffer = static_cast<char*>(malloc(length));
if (buffer == nullptr) {
TT_LOG_E(TAG, LOG_MESSAGE_ALLOC_FAILED_FMT, length);
return nullptr;
}
while (bytesRead < length) {
size_t read_size = length - bytesRead;
size_t bytes_received = httpd_req_recv(request, result.get() + bytesRead, read_size);
size_t bytes_received = httpd_req_recv(request, buffer + bytesRead, read_size);
if (bytes_received <= 0) {
TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length);
return nullptr;
@ -86,7 +92,7 @@ std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, si
bytesRead += bytes_received;
}
return result;
return std::unique_ptr<char[]>(std::move(buffer));
}
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) {

View File

@ -194,9 +194,16 @@ esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
return ESP_FAIL;
}
app::start(id_key_pos->second);
const auto& app_id = id_key_pos->second;
if (app::isRunning(app_id)) {
app::stopAll(app_id);
}
app::start(app_id);
TT_LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
httpd_resp_send(request, nullptr, 0);
return ESP_OK;
}

View File

@ -1,25 +1,25 @@
#include "Tactility/service/gui/GuiService.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/lvgl/Statusbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/service/gui/GuiService.h>
#include <Tactility/Tactility.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/Tactility.h>
namespace tt::service::gui {
extern const ServiceManifest manifest;
constexpr auto* TAG = "GuiService";
using namespace loader;
// region AppManifest
void GuiService::onLoaderEvent(loader::LoaderEvent event) {
if (event == loader::LoaderEvent::ApplicationShowing) {
auto app_instance = app::getCurrentAppContext();
void GuiService::onLoaderEvent(LoaderService::Event event) {
if (event == LoaderService::Event::ApplicationShowing) {
auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
showApp(app_instance);
} else if (event == loader::LoaderEvent::ApplicationHiding) {
} else if (event == LoaderService::Event::ApplicationHiding) {
hideApp();
}
}
@ -125,7 +125,9 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
[]() { return guiMain(); }
);
loader_pubsub_subscription = loader::getPubsub()->subscribe([this](auto event) {
const auto loader = findLoaderService();
assert(loader != nullptr);
loader_pubsub_subscription = loader->getPubsub()->subscribe([this](auto event) {
onLoaderEvent(event);
});
@ -168,7 +170,9 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
void GuiService::onStop(TT_UNUSED ServiceContext& service) {
lock();
loader::getPubsub()->unsubscribe(loader_pubsub_subscription);
const auto loader = findLoaderService();
assert(loader != nullptr);
loader->getPubsub()->unsubscribe(loader_pubsub_subscription);
appToRender = nullptr;
isStarted = false;
@ -190,36 +194,50 @@ void GuiService::requestDraw() {
Thread::setFlags(thread_id, GUI_THREAD_FLAG_DRAW);
}
void GuiService::showApp(std::shared_ptr<app::AppContext> app) {
lock();
void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) {
TT_LOG_W(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
} else {
// Ensure previous app triggers onHide() logic
if (appToRender != nullptr) {
hideApp();
}
appToRender = std::move(app);
TT_LOG_E(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
return;
}
unlock();
if (appToRender != nullptr && appToRender->getLaunchId() == app->getLaunchId()) {
TT_LOG_W(TAG, "Already showing %s", app->getManifest().appId.c_str());
return;
}
TT_LOG_I(TAG, "Showing %s", app->getManifest().appId.c_str());
// Ensure previous app triggers onHide() logic
if (appToRender != nullptr) {
hideApp();
}
appToRender = std::move(app);
requestDraw();
}
void GuiService::hideApp() {
lock();
auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) {
TT_LOG_W(TAG, "Failed to hide app: GUI not started");
} else if (appToRender == nullptr) {
TT_LOG_W(TAG, "hideApp() called but no app is currently shown");
} else {
// We must lock the LVGL port, because the viewport hide callbacks
// might call LVGL APIs (e.g. to remove the keyboard from the screen root)
tt_check(lvgl::lock(configTICK_RATE_HZ));
appToRender->getApp()->onHide(*appToRender);
lvgl::unlock();
appToRender = nullptr;
TT_LOG_E(TAG, "Failed to hide app: GUI not started");
return;
}
unlock();
if (appToRender == nullptr) {
TT_LOG_W(TAG, "hideApp() called but no app is currently shown");
return;
}
// We must lock the LVGL port, because the viewport hide callbacks
// might call LVGL APIs (e.g. to remove the keyboard from the screen root)
lvgl::lock(portMAX_DELAY);
appToRender->getApp()->onHide(*appToRender);
lvgl::unlock();
appToRender = nullptr;
}
std::shared_ptr<GuiService> findService() {

View File

@ -1,19 +1,17 @@
#include "Tactility/service/loader/Loader.h"
#include "Tactility/app/AppInstance.h"
#include "Tactility/app/AppManifest.h"
#include "Tactility/app/AppRegistration.h"
#include <Tactility/service/loader/Loader.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/DispatcherThread.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <stack>
#include <vector>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#include <utility>
#else
#include "Tactility/lvgl/LvglSync.h"
#endif
namespace tt::service::loader {
@ -21,6 +19,7 @@ namespace tt::service::loader {
constexpr auto* TAG = "Loader";
constexpr auto LOADER_TIMEOUT = (100 / portTICK_PERIOD_MS);
// Forward declaration
extern const ServiceManifest manifest;
static const char* appStateToString(app::State state) {
@ -28,63 +27,19 @@ static const char* appStateToString(app::State state) {
using enum app::State;
case Initial:
return "initial";
case Started:
case Created:
return "started";
case Showing:
return "showing";
case Hiding:
return "hiding";
case Stopped:
case Destroyed:
return "stopped";
default:
return "?";
}
}
// region AppManifest
class LoaderService final : public Service {
std::shared_ptr<PubSub<LoaderEvent>> pubsubExternal = std::make_shared<PubSub<LoaderEvent>>();
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::stack<std::shared_ptr<app::AppInstance>> appStack;
app::LaunchId nextLaunchId = 0;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw()
*/
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters);
void onStopAppMessage(const std::string& id);
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
public:
bool onStart(TT_UNUSED ServiceContext& service) override {
dispatcherThread->start();
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
// Send stop signal to thread and wait for thread to finish
mutex.withLock([this] {
dispatcherThread->stop();
});
}
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters);
void stopApp();
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
std::shared_ptr<PubSub<LoaderEvent>> getPubsub() const { return pubsubExternal; }
};
std::shared_ptr<LoaderService> _Nullable optScreenshotService() {
return service::findServiceById<LoaderService>(manifest.id);
}
void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start by id %s", id.c_str());
@ -100,26 +55,22 @@ void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launc
return;
}
auto previous_app = !appStack.empty() ? appStack.top() : nullptr;
auto previous_app = !appStack.empty() ? appStack[appStack.size() - 1]: nullptr;
auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters);
new_app->mutableFlags().hideStatusbar = (app_manifest->appFlags & app::AppManifest::Flags::HideStatusBar);
appStack.push(new_app);
transitionAppToState(new_app, app::State::Initial);
transitionAppToState(new_app, app::State::Started);
// We might have to hide the previous app first
if (previous_app != nullptr) {
transitionAppToState(previous_app, app::State::Hiding);
}
appStack.push_back(new_app);
transitionAppToState(new_app, app::State::Created);
transitionAppToState(new_app, app::State::Showing);
pubsubExternal->publish(LoaderEvent::ApplicationStarted);
}
void LoaderService::onStopAppMessage(const std::string& id) {
void LoaderService::onStopTopAppMessage(const std::string& id) {
auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
@ -134,7 +85,7 @@ void LoaderService::onStopAppMessage(const std::string& id) {
}
// Stop current app
auto app_to_stop = appStack.top();
auto app_to_stop = appStack[appStack.size() - 1];
if (app_to_stop->getManifest().appId != id) {
TT_LOG_E(TAG, "Stop app: id mismatch (wanted %s but found %s on top of stack)", id.c_str(), app_to_stop->getManifest().appId.c_str());
@ -156,9 +107,9 @@ void LoaderService::onStopAppMessage(const std::string& id) {
auto app_to_stop_launch_id = app_to_stop->getLaunchId();
transitionAppToState(app_to_stop, app::State::Hiding);
transitionAppToState(app_to_stop, app::State::Stopped);
transitionAppToState(app_to_stop, app::State::Destroyed);
appStack.pop();
appStack.pop_back();
// We only expect the app to be referenced within the current scope
if (app_to_stop.use_count() > 1) {
@ -177,7 +128,7 @@ void LoaderService::onStopAppMessage(const std::string& id) {
std::shared_ptr<app::AppInstance> instance_to_resume;
// If there's a previous app, resume it
if (!appStack.empty()) {
instance_to_resume = appStack.top();
instance_to_resume = appStack[appStack.size() - 1];
assert(instance_to_resume);
transitionAppToState(instance_to_resume, app::State::Showing);
}
@ -186,8 +137,6 @@ void LoaderService::onStopAppMessage(const std::string& id) {
lock.unlock();
// WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock!
pubsubExternal->publish(LoaderEvent::ApplicationStopped);
if (instance_to_resume != nullptr) {
if (result_set) {
if (result_bundle != nullptr) {
@ -206,7 +155,6 @@ void LoaderService::onStopAppMessage(const std::string& id) {
);
}
} else {
const Bundle empty_bundle;
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
@ -217,6 +165,70 @@ void LoaderService::onStopAppMessage(const std::string& id) {
}
}
int LoaderService::findAppInStack(const std::string& id) const {
auto lock = mutex.asScopedLock();
lock.lock();
for (size_t i = 0; i < appStack.size(); i++) {
if (appStack[i]->getManifest().appId == id) {
return i;
}
}
return -1;
}
void LoaderService::onStopAllAppMessage(const std::string& id) {
auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return;
}
if (!isRunning(id)) {
TT_LOG_E(TAG, "Stop all: %s not running", id.c_str());
return;
}
int app_to_stop_index = findAppInStack(id);
if (app_to_stop_index < 0) {
TT_LOG_E(TAG, "Stop all: %s not found in stack", id.c_str());
return;
}
// Find an app to resume, if any
std::shared_ptr<app::AppInstance> instance_to_resume;
if (app_to_stop_index > 0) {
instance_to_resume = appStack[app_to_stop_index - 1];
assert(instance_to_resume);
}
// Stop all apps and find the LaunchId of the last-closed app, so we can call onResult() if needed
app::LaunchId last_launch_id = 0;
for (int i = appStack.size() - 1; i >= app_to_stop_index; i--) {
auto app_to_stop = appStack[i];
// Hide the app first in case it's still being shown
if (app_to_stop->getState() == app::State::Showing) {
transitionAppToState(app_to_stop, app::State::Hiding);
}
transitionAppToState(app_to_stop, app::State::Destroyed);
last_launch_id = app_to_stop->getLaunchId();
appStack.pop_back();
}
if (instance_to_resume != nullptr) {
TT_LOG_I(TAG, "Resuming %s", instance_to_resume->getManifest().appId.c_str());
transitionAppToState(instance_to_resume, app::State::Showing);
instance_to_resume->getApp()->onResult(
*instance_to_resume,
last_launch_id,
app::Result::Cancelled,
nullptr
);
}
}
void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state) {
const app::AppManifest& app_manifest = app->getManifest();
const app::State old_state = app->getState();
@ -232,89 +244,87 @@ void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>
switch (state) {
using enum app::State;
case Initial:
break;
case Started:
tt_crash(LOG_MESSAGE_ILLEGAL_STATE);
case Created:
assert(app->getState() == app::State::Initial);
app->getApp()->onCreate(*app);
pubsubExternal->publish(Event::ApplicationStarted);
break;
case Showing: {
pubsubExternal->publish(LoaderEvent::ApplicationShowing);
assert(app->getState() == app::State::Hiding || app->getState() == app::State::Created);
pubsubExternal->publish(Event::ApplicationShowing);
break;
}
case Hiding: {
pubsubExternal->publish(LoaderEvent::ApplicationHiding);
assert(app->getState() == app::State::Showing);
pubsubExternal->publish(Event::ApplicationHiding);
break;
}
case Stopped:
// TODO: Verify manifest
case Destroyed:
app->getApp()->onDestroy(*app);
pubsubExternal->publish(Event::ApplicationStopped);
break;
}
app->setState(state);
}
app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
auto launch_id = nextLaunchId++;
app::LaunchId LoaderService::start(const std::string& id, std::shared_ptr<const Bundle> parameters) {
const auto launch_id = nextLaunchId++;
dispatcherThread->dispatch([this, id, launch_id, parameters]() {
onStartAppMessage(id, launch_id, parameters);
});
return launch_id;
}
void LoaderService::stopApp() {
TT_LOG_I(TAG, "stopApp()");
auto id = getCurrentAppContext()->getManifest().appId;
dispatcherThread->dispatch([this, id]() {
onStopAppMessage(id);
void LoaderService::stopTop() {
const auto& id = getCurrentAppContext()->getManifest().appId;
stopTop(id);
}
void LoaderService::stopTop(const std::string& id) {
TT_LOG_I(TAG, "dispatching stopTop(%s)", id.c_str());
dispatcherThread->dispatch([this, id] {
onStopTopAppMessage(id);
});
}
void LoaderService::stopAll(const std::string& id) {
TT_LOG_I(TAG, "dispatching stopAll(%s)", id.c_str());
dispatcherThread->dispatch([this, id] {
onStopAllAppMessage(id);
});
TT_LOG_I(TAG, "dispatched");
}
std::shared_ptr<app::AppContext> _Nullable LoaderService::getCurrentAppContext() {
auto lock = mutex.asScopedLock();
const auto lock = mutex.asScopedLock();
lock.lock();
return appStack.top();
if (appStack.empty()) {
return nullptr;
} else {
return appStack[appStack.size() - 1];
}
}
// region Public API
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start app %s", id.c_str());
auto service = optScreenshotService();
assert(service);
return service->startApp(id, std::move(parameters));
bool LoaderService::isRunning(const std::string& id) const {
const auto lock = mutex.asScopedLock();
lock.lock();
for (const auto& app : appStack) {
if (app->getManifest().appId == id) {
return true;
}
}
return false;
}
void stopApp() {
TT_LOG_I(TAG, "Stop app");
auto service = optScreenshotService();
service->stopApp();
std::shared_ptr<LoaderService> _Nullable findLoaderService() {
return service::findServiceById<LoaderService>(manifest.id);
}
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
auto service = optScreenshotService();
assert(service);
return service->getCurrentAppContext();
}
std::shared_ptr<app::App> _Nullable getCurrentApp() {
auto app_context = getCurrentAppContext();
return app_context != nullptr ? app_context->getApp() : nullptr;
}
std::shared_ptr<PubSub<LoaderEvent>> getPubsub() {
auto service = optScreenshotService();
assert(service);
return service->getPubsub();
}
// endregion Public API
extern const ServiceManifest manifest = {
.id = "Loader",
.createService = create<LoaderService>
};
// endregion
} // namespace

View File

@ -4,8 +4,12 @@
*/
#pragma once
// Crashes
#define LOG_MESSAGE_ILLEGAL_STATE "Illegal state"
// Alloc
#define LOG_MESSAGE_ALLOC_FAILED "Memory allocation failed"
#define LOG_MESSAGE_ALLOC_FAILED "Out of memory"
#define LOG_MESSAGE_ALLOC_FAILED_FMT "Out of memory (failed to allocated %zu bytes)"
// Mutex
#define LOG_MESSAGE_MUTEX_LOCK_FAILED "Mutex acquisition timeout"