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

View File

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

View File

@ -2,14 +2,12 @@
## Higher Priority ## 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. - Make a URL handler. Use it for handling local files. Match file types with apps.
Create some kind of "intent" handler like on Android. 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 intent can have an action (e.g. view), a URL and an optional bundle.
The manifest can provide the intent handler 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. - 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. 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. 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. 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 direct installation of an `.app` file with `tactility.py install helloworld.app <ip>`
- Support `tactility.py target <ip>` to remember the device IP address. - 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 ## 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) - 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 - 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. - 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 ## 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 - Implement system suspend that turns off the screen
- The boot button on some devices can be used as GPIO_NUM_0 at runtime - The boot button on some devices can be used as GPIO_NUM_0 at runtime
- Localize all apps - 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. */ /** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stop(); 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) */ /** @return the currently running app context (it is only ever null before the splash screen is shown) */
std::shared_ptr<AppContext> _Nullable getCurrentAppContext(); std::shared_ptr<AppContext> _Nullable getCurrentAppContext();

View File

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

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h> #include <Tactility/Bundle.h>
#include <string> #include <string>
@ -11,7 +12,7 @@
*/ */
namespace tt::app::inputdialog { 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 * @return the text that was in the field when OK was pressed, or otherwise empty string

View File

@ -1,11 +1,14 @@
#pragma once #pragma once
#include <Tactility/app/App.h>
namespace tt::app::notes { namespace tt::app::notes {
/** /**
* Start the notes app with the specified text file. * Start the notes app with the specified text file.
* @param[in] filePath the path to the text file to open * @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 #pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h> #include <Tactility/Bundle.h>
#include <string> #include <string>
@ -15,7 +16,7 @@
*/ */
namespace tt::app::selectiondialog { 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. * Get the index of the item that the user selected.

View File

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

View File

@ -1,45 +1,99 @@
#pragma once #pragma once
#include "Tactility/app/AppManifest.h" #include <Tactility/app/AppInstance.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/Bundle.h> #include <Tactility/Bundle.h>
#include <Tactility/DispatcherThread.h>
#include <Tactility/PubSub.h> #include <Tactility/PubSub.h>
#include <Tactility/service/Service.h>
#include <memory> #include <memory>
namespace tt::service::loader { namespace tt::service::loader {
// region LoaderEvent for PubSub
enum class LoaderEvent{ class LoaderService final : public Service {
ApplicationStarted,
ApplicationShowing, public:
ApplicationHiding,
ApplicationStopped 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 std::shared_ptr<LoaderService> _Nullable findLoaderService();
/**
* @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();
} // namespace } // namespace

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ public:
/** /**
* Start the app with optional pre-filled fields. * 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); bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid);

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include "Tactility/service/Service.h" #include <Tactility/service/Service.h>
#include <Tactility/Mutex.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_EXIT (1 << 2)
#define GUI_THREAD_FLAG_ALL (GUI_THREAD_FLAG_DRAW | GUI_THREAD_FLAG_INPUT | GUI_THREAD_FLAG_EXIT) #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 and lock
Thread* thread = nullptr; Thread* thread = nullptr;
Mutex mutex = Mutex(Mutex::Type::Recursive); 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 // Layers and Canvas
lv_obj_t* appRootWidget = nullptr; lv_obj_t* appRootWidget = nullptr;
lv_obj_t* statusbarWidget = nullptr; lv_obj_t* statusbarWidget = nullptr;
// App-specific // App-specific
std::shared_ptr<app::AppContext> appToRender = nullptr; std::shared_ptr<app::AppInstance> appToRender = nullptr;
lv_obj_t* _Nullable keyboard = nullptr; lv_obj_t* _Nullable keyboard = nullptr;
lv_group_t* keyboardGroup = nullptr; lv_group_t* keyboardGroup = nullptr;
@ -38,7 +38,7 @@ class GuiService : public Service {
static int32_t guiMain(); static int32_t guiMain();
void onLoaderEvent(loader::LoaderEvent event); void onLoaderEvent(loader::LoaderService::Event event);
lv_obj_t* createAppViews(lv_obj_t* parent); lv_obj_t* createAppViews(lv_obj_t* parent);
@ -52,7 +52,7 @@ class GuiService : public Service {
tt_check(mutex.unlock()); tt_check(mutex.unlock());
} }
void showApp(std::shared_ptr<app::AppContext> app); void showApp(std::shared_ptr<app::AppInstance> app);
void hideApp(); void hideApp();

View File

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

View File

@ -3,20 +3,47 @@
namespace tt::app { namespace tt::app {
constexpr auto* TAG = "App";
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) { 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() { 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() { 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() { 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; return false;
} }
// If the app was already running, then stop it
if (isRunning(manifest.appId)) {
stopAll(manifest.appId);
}
target_path_lock.lock(); target_path_lock.lock();
const std::string renamed_target_path = std::format("{}/{}", app_parent_path, manifest.appId); const std::string renamed_target_path = std::format("{}/{}", app_parent_path, manifest.appId);
if (file::isDirectory(renamed_target_path)) { if (file::isDirectory(renamed_target_path)) {
@ -202,6 +207,12 @@ bool install(const std::string& path) {
bool uninstall(const std::string& appId) { bool uninstall(const std::string& appId) {
TT_LOG_I(TAG, "Uninstalling app %s", appId.c_str()); 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); auto app_path = getAppInstallPath(appId);
return file::withLock<bool>(app_path, [&app_path, &appId] { return file::withLock<bool>(app_path, [&app_path, &appId] {
if (!file::isDirectory(app_path)) { if (!file::isDirectory(app_path)) {

View File

@ -23,6 +23,8 @@ static std::string getErrorCodeString(int error_code) {
return "out of memory"; return "out of memory";
case ENOSYS: case ENOSYS:
return "missing symbol"; return "missing symbol";
case EINVAL:
return "invalid argument or main() missing";
default: default:
return std::format("code {}", error_code); return std::format("code {}", error_code);
} }
@ -137,14 +139,14 @@ public:
staticParametersSetCount = 0; staticParametersSetCount = 0;
if (!startElf()) { if (!startElf()) {
service::loader::stopApp(); stop();
auto message = lastError.empty() ? "Application failed to start." : std::format("Application failed to start: {}", lastError); auto message = lastError.empty() ? "Application failed to start." : std::format("Application failed to start: {}", lastError);
alertdialog::start("Error", message); alertdialog::start("Error", message);
return; return;
} }
if (staticParametersSetCount == 0) { if (staticParametersSetCount == 0) {
service::loader::stopApp(); stop();
alertdialog::start("Error", "Application failed to start: application failed to register itself"); alertdialog::start("Error", "Application failed to start: application failed to register itself");
return; 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_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined); 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) { 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_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK"); 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) { int32_t getResultIndex(const Bundle& bundle) {
@ -72,9 +72,9 @@ private:
auto bundle = std::make_unique<Bundle>(); auto bundle = std::make_unique<Bundle>();
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index); 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) { 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/app/AppRegistration.h>
#include "Tactility/service/loader/Loader.h" #include <Tactility/service/loader/Loader.h>
#include "Tactility/lvgl/Toolbar.h" #include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Assets.h> #include <Tactility/Assets.h>
@ -9,11 +9,11 @@
namespace tt::app::applist { namespace tt::app::applist {
class AppListApp : public App { class AppListApp final : public App {
static void onAppPressed(lv_event_t* e) { static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(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) { static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
#include "Tactility/app/AppRegistration.h" #include <Tactility/app/AppRegistration.h>
#include "Tactility/lvgl/Toolbar.h" #include <Tactility/lvgl/Toolbar.h>
#include "Tactility/service/loader/Loader.h" #include <Tactility/service/loader/Loader.h>
#include <Tactility/Assets.h> #include <Tactility/Assets.h>
#include <Tactility/Check.h> #include <Tactility/Check.h>
@ -12,7 +12,7 @@ namespace tt::app::settings {
static void onAppPressed(lv_event_t* e) { static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(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) { 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()); 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 { void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); 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); lv_obj_set_flex_grow(list, 1);
auto manifests = getApps(); auto manifests = getApps();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); std::ranges::sort(manifests, SortAppManifestByName);
for (const auto& manifest: manifests) { for (const auto& manifest: manifests) {
if (manifest->appCategory == Category::Settings) { if (manifest->appCategory == Category::Settings) {
createWidget(manifest, list); createWidget(manifest, list);

View File

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

View File

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

View File

@ -33,7 +33,7 @@ void WifiConnect::onWifiEvent(service::wifi::WifiEvent event) {
case service::wifi::WifiEvent::ConnectionSuccess: case service::wifi::WifiEvent::ConnectionSuccess:
if (getState().isConnecting()) { if (getState().isConnecting()) {
state.setConnecting(false); state.setConnecting(false);
service::loader::stopApp(); stop(manifest.appId);
} }
break; break;
default: default:
@ -102,11 +102,11 @@ extern const AppManifest manifest = {
.createApp = create<WifiConnect> .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>(); auto parameters = std::make_shared<Bundle>();
parameters->putString(WIFI_CONNECT_PARAM_SSID, ssid); parameters->putString(WIFI_CONNECT_PARAM_SSID, ssid);
parameters->putString(WIFI_CONNECT_PARAM_PASSWORD, password); 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) { 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> .createApp = create<WifiManage>
}; };
void start() { LaunchId start() {
service::loader::startApp(manifest.appId); return app::start(manifest.appId);
} }
} // namespace } // 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) { 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) { 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 <memory>
#include <ranges> #include <ranges>
@ -8,10 +8,10 @@
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#define TAG "network"
namespace tt::network { namespace tt::network {
constexpr auto* TAG = "HttpdReq";
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) { 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()); size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str());
if (header_size == 0) { if (header_size == 0) {
@ -73,11 +73,17 @@ std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, si
assert(length > 0); assert(length > 0);
bytesRead = 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) { while (bytesRead < length) {
size_t read_size = length - bytesRead; 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) { if (bytes_received <= 0) {
TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length); TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length);
return nullptr; return nullptr;
@ -86,7 +92,7 @@ std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, si
bytesRead += bytes_received; bytesRead += bytes_received;
} }
return result; return std::unique_ptr<char[]>(std::move(buffer));
} }
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) { 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; 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()); TT_LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
httpd_resp_send(request, nullptr, 0); httpd_resp_send(request, nullptr, 0);
return ESP_OK; return ESP_OK;
} }

View File

@ -1,25 +1,25 @@
#include "Tactility/service/gui/GuiService.h" #include <Tactility/service/gui/GuiService.h>
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/lvgl/Statusbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/Tactility.h>
#include <Tactility/app/AppInstance.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/service/ServiceRegistration.h>
#include <Tactility/Tactility.h>
namespace tt::service::gui { namespace tt::service::gui {
extern const ServiceManifest manifest; extern const ServiceManifest manifest;
constexpr auto* TAG = "GuiService"; constexpr auto* TAG = "GuiService";
using namespace loader;
// region AppManifest // region AppManifest
void GuiService::onLoaderEvent(loader::LoaderEvent event) { void GuiService::onLoaderEvent(LoaderService::Event event) {
if (event == loader::LoaderEvent::ApplicationShowing) { if (event == LoaderService::Event::ApplicationShowing) {
auto app_instance = app::getCurrentAppContext(); auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
showApp(app_instance); showApp(app_instance);
} else if (event == loader::LoaderEvent::ApplicationHiding) { } else if (event == LoaderService::Event::ApplicationHiding) {
hideApp(); hideApp();
} }
} }
@ -125,7 +125,9 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
[]() { return guiMain(); } []() { 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); onLoaderEvent(event);
}); });
@ -168,7 +170,9 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
void GuiService::onStop(TT_UNUSED ServiceContext& service) { void GuiService::onStop(TT_UNUSED ServiceContext& service) {
lock(); lock();
loader::getPubsub()->unsubscribe(loader_pubsub_subscription); const auto loader = findLoaderService();
assert(loader != nullptr);
loader->getPubsub()->unsubscribe(loader_pubsub_subscription);
appToRender = nullptr; appToRender = nullptr;
isStarted = false; isStarted = false;
@ -190,36 +194,50 @@ void GuiService::requestDraw() {
Thread::setFlags(thread_id, GUI_THREAD_FLAG_DRAW); Thread::setFlags(thread_id, GUI_THREAD_FLAG_DRAW);
} }
void GuiService::showApp(std::shared_ptr<app::AppContext> app) { void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
lock(); auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) { if (!isStarted) {
TT_LOG_W(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str()); TT_LOG_E(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
} else { return;
// Ensure previous app triggers onHide() logic
if (appToRender != nullptr) {
hideApp();
}
appToRender = std::move(app);
} }
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(); requestDraw();
} }
void GuiService::hideApp() { void GuiService::hideApp() {
lock(); auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) { if (!isStarted) {
TT_LOG_W(TAG, "Failed to hide app: GUI not started"); TT_LOG_E(TAG, "Failed to hide app: GUI not started");
} else if (appToRender == nullptr) { return;
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;
} }
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() { std::shared_ptr<GuiService> findService() {

View File

@ -1,19 +1,17 @@
#include "Tactility/service/loader/Loader.h" #include <Tactility/service/loader/Loader.h>
#include "Tactility/app/AppInstance.h" #include <Tactility/app/AppInstance.h>
#include "Tactility/app/AppManifest.h" #include <Tactility/app/AppManifest.h>
#include "Tactility/app/AppRegistration.h" #include <Tactility/app/AppRegistration.h>
#include <Tactility/DispatcherThread.h> #include <Tactility/DispatcherThread.h>
#include <Tactility/service/ServiceManifest.h> #include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h> #include <Tactility/service/ServiceRegistration.h>
#include <stack> #include <vector>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <utility> #include <utility>
#else
#include "Tactility/lvgl/LvglSync.h"
#endif #endif
namespace tt::service::loader { namespace tt::service::loader {
@ -21,6 +19,7 @@ namespace tt::service::loader {
constexpr auto* TAG = "Loader"; constexpr auto* TAG = "Loader";
constexpr auto LOADER_TIMEOUT = (100 / portTICK_PERIOD_MS); constexpr auto LOADER_TIMEOUT = (100 / portTICK_PERIOD_MS);
// Forward declaration
extern const ServiceManifest manifest; extern const ServiceManifest manifest;
static const char* appStateToString(app::State state) { static const char* appStateToString(app::State state) {
@ -28,63 +27,19 @@ static const char* appStateToString(app::State state) {
using enum app::State; using enum app::State;
case Initial: case Initial:
return "initial"; return "initial";
case Started: case Created:
return "started"; return "started";
case Showing: case Showing:
return "showing"; return "showing";
case Hiding: case Hiding:
return "hiding"; return "hiding";
case Stopped: case Destroyed:
return "stopped"; return "stopped";
default: default:
return "?"; 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) { 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()); 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; 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); auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters);
new_app->mutableFlags().hideStatusbar = (app_manifest->appFlags & app::AppManifest::Flags::HideStatusBar); 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 // We might have to hide the previous app first
if (previous_app != nullptr) { if (previous_app != nullptr) {
transitionAppToState(previous_app, app::State::Hiding); transitionAppToState(previous_app, app::State::Hiding);
} }
appStack.push_back(new_app);
transitionAppToState(new_app, app::State::Created);
transitionAppToState(new_app, app::State::Showing); 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(); auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) { if (!lock.lock(LOADER_TIMEOUT)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
@ -134,7 +85,7 @@ void LoaderService::onStopAppMessage(const std::string& id) {
} }
// Stop current app // Stop current app
auto app_to_stop = appStack.top(); auto app_to_stop = appStack[appStack.size() - 1];
if (app_to_stop->getManifest().appId != id) { 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()); 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(); auto app_to_stop_launch_id = app_to_stop->getLaunchId();
transitionAppToState(app_to_stop, app::State::Hiding); 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 // We only expect the app to be referenced within the current scope
if (app_to_stop.use_count() > 1) { 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; std::shared_ptr<app::AppInstance> instance_to_resume;
// If there's a previous app, resume it // If there's a previous app, resume it
if (!appStack.empty()) { if (!appStack.empty()) {
instance_to_resume = appStack.top(); instance_to_resume = appStack[appStack.size() - 1];
assert(instance_to_resume); assert(instance_to_resume);
transitionAppToState(instance_to_resume, app::State::Showing); transitionAppToState(instance_to_resume, app::State::Showing);
} }
@ -186,8 +137,6 @@ void LoaderService::onStopAppMessage(const std::string& id) {
lock.unlock(); lock.unlock();
// WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock! // 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 (instance_to_resume != nullptr) {
if (result_set) { if (result_set) {
if (result_bundle != nullptr) { if (result_bundle != nullptr) {
@ -206,7 +155,6 @@ void LoaderService::onStopAppMessage(const std::string& id) {
); );
} }
} else { } else {
const Bundle empty_bundle;
instance_to_resume->getApp()->onResult( instance_to_resume->getApp()->onResult(
*instance_to_resume, *instance_to_resume,
app_to_stop_launch_id, 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) { void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state) {
const app::AppManifest& app_manifest = app->getManifest(); const app::AppManifest& app_manifest = app->getManifest();
const app::State old_state = app->getState(); const app::State old_state = app->getState();
@ -232,89 +244,87 @@ void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>
switch (state) { switch (state) {
using enum app::State; using enum app::State;
case Initial: case Initial:
break; tt_crash(LOG_MESSAGE_ILLEGAL_STATE);
case Started: case Created:
assert(app->getState() == app::State::Initial);
app->getApp()->onCreate(*app); app->getApp()->onCreate(*app);
pubsubExternal->publish(Event::ApplicationStarted);
break; break;
case Showing: { case Showing: {
pubsubExternal->publish(LoaderEvent::ApplicationShowing); assert(app->getState() == app::State::Hiding || app->getState() == app::State::Created);
pubsubExternal->publish(Event::ApplicationShowing);
break; break;
} }
case Hiding: { case Hiding: {
pubsubExternal->publish(LoaderEvent::ApplicationHiding); assert(app->getState() == app::State::Showing);
pubsubExternal->publish(Event::ApplicationHiding);
break; break;
} }
case Stopped: case Destroyed:
// TODO: Verify manifest
app->getApp()->onDestroy(*app); app->getApp()->onDestroy(*app);
pubsubExternal->publish(Event::ApplicationStopped);
break; break;
} }
app->setState(state); app->setState(state);
} }
app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) { app::LaunchId LoaderService::start(const std::string& id, std::shared_ptr<const Bundle> parameters) {
auto launch_id = nextLaunchId++; const auto launch_id = nextLaunchId++;
dispatcherThread->dispatch([this, id, launch_id, parameters]() { dispatcherThread->dispatch([this, id, launch_id, parameters]() {
onStartAppMessage(id, launch_id, parameters); onStartAppMessage(id, launch_id, parameters);
}); });
return launch_id; return launch_id;
} }
void LoaderService::stopApp() { void LoaderService::stopTop() {
TT_LOG_I(TAG, "stopApp()"); const auto& id = getCurrentAppContext()->getManifest().appId;
auto id = getCurrentAppContext()->getManifest().appId; stopTop(id);
dispatcherThread->dispatch([this, id]() { }
onStopAppMessage(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() { std::shared_ptr<app::AppContext> _Nullable LoaderService::getCurrentAppContext() {
auto lock = mutex.asScopedLock(); const auto lock = mutex.asScopedLock();
lock.lock(); lock.lock();
return appStack.top(); if (appStack.empty()) {
return nullptr;
} else {
return appStack[appStack.size() - 1];
}
} }
// region Public API bool LoaderService::isRunning(const std::string& id) const {
const auto lock = mutex.asScopedLock();
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) { lock.lock();
TT_LOG_I(TAG, "Start app %s", id.c_str()); for (const auto& app : appStack) {
auto service = optScreenshotService(); if (app->getManifest().appId == id) {
assert(service); return true;
return service->startApp(id, std::move(parameters)); }
}
return false;
} }
void stopApp() { std::shared_ptr<LoaderService> _Nullable findLoaderService() {
TT_LOG_I(TAG, "Stop app"); return service::findServiceById<LoaderService>(manifest.id);
auto service = optScreenshotService();
service->stopApp();
} }
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 = { extern const ServiceManifest manifest = {
.id = "Loader", .id = "Loader",
.createService = create<LoaderService> .createService = create<LoaderService>
}; };
// endregion
} // namespace } // namespace

View File

@ -4,8 +4,12 @@
*/ */
#pragma once #pragma once
// Crashes
#define LOG_MESSAGE_ILLEGAL_STATE "Illegal state"
// Alloc // 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 // Mutex
#define LOG_MESSAGE_MUTEX_LOCK_FAILED "Mutex acquisition timeout" #define LOG_MESSAGE_MUTEX_LOCK_FAILED "Mutex acquisition timeout"